例のロボットを今度はHaskellで動かした

Pocket
LINEで送る

今日は某氏が取り仕切るMaker Faire Tokyo 2015に遊びに行きました。大いに啓蒙されたので、帰宅後、俺も何かやろうということで、「Raspberry Pi Mouseはどの言語でも動かせるアピール」の一環としてHaskellでRaspberry Pi Mouseを動かしてみました。

Raspberry Pi Mouseというのは日経Linuxの連載「Raspberry Piで始めるかんたんロボット製作」で作っているロボットです。よく「パソコンのカーソル動かす方のマウス」と間違われるのですが、ここで言っている「マウス」というのはロボット競技のマイクロマウスの「マウス」です。

このロボットをHaskellで動かすわけですが、そのためにはセンサの読み込みとモータの動作を非同期で行う必要があります。私みたいな阿呆でも分かる解説を探していたところ、

Haskellでマルチスレッド処理 – Qiita

が大変分かりやすかったので参考にさせていただきました。

ちゃんとやるにはこの本↓も参考になるかもしれません。(amazonに飛びます。)

コードの前に動いたロボットの様子をお見せすると、

というように、前進して壁を検知したら止まるという動作ができました。たいしたことはやってませんが、滑らかに動きつつセンサの値を読んでいるのがミソです。

このムービーで使ったコードはミニマムなサンプルにして、GitHubにアップしてあります。これで

  • シェルスクリプト(bash)
  • C/C++
  • Python
  • Haskell

と、自分がよく使う4大言語で動きました。デバイスファイルに字を出し入れするだけで動くので当たり前ですが・・・。たぶん、もうやらなくても十分アピールになるかなあ・・・。

Raspberry PiでHaskellを使うときはsudo apt-get install ghcします。

コード

コードは以下のような感じです。もう一度書いておくと、Haskellでマルチスレッド処理 – Qiita のコード(サンプルコードその2)の影響を色濃く受けています。

大雑把な説明ですが、モータを動かすforward関数と、センサ値を読み出すreadSensorという関数がロボットに直接関与しています(デバイスファイルを読み書きしている)。こいつらは両方最後に自分を呼び出していて、readSensorだけ、センサ値が閾値を超えると、その旨をputMVarという関数でrefに反映して処理を終わっています。

forwardとreadSensorを動かしているのはmain関数のにあるforkIOで、これが非同期に上記2つの関数を実行します。watchSensor関数はmainで呼ばれ、refの値を監視してTrueだったらforwardの処理を止め、そうでなかったら再度自分自身を呼び出しています。refの型はMVar Boolで、これは非同期で動くスレッド間で安全に読み書きできるそうです。間違ってたらアレなので、その筋の方のブログや書籍を参考にどうぞ。

import Control.Concurrent
import Control.Monad
import System.Posix.Unistd

-- 単純に前進する関数
forward :: IO ()
forward = do
  -- 車輪を400Hzで0.1秒回す
  writeFile "/dev/rtmotor0" "400 400 100"
  -- 無限ループ(・・・という言い方は不適切か?)
  forward

-- 距離センサを読み出す
readSensor :: MVar Bool -> IO ()
readSensor ref = do
  -- センサを休める
  threadDelay 200000
  cs <- readFile "/dev/rtlightsensor0"
  -- 4つのセンサの値を合計
  let v = sum [ read w :: Int | w <- words cs ]
  -- センサが反応していなかったら再度計測
  if v > 1000 then putMVar ref True else readSensor ref

-- センサに何か反応したらモータのスレッドを止める
watchSensor :: MVar Bool -> ThreadId -> IO ()
watchSensor ref w = do
  tf <- takeMVar ref
  if tf then killThread w else watchSensor ref w

main = do
  ref <- newMVar False
  w <- forkIO $ forward
  _ <- forkIO $ readSensor ref

  watchSensor ref w

この部分だけ見ると、Haskellなのに手続き的なのですが、ここにもっと高度な処理を関数で書いていけるのならば、Haskellで書くメリットも出てくるかもしれません。個人的にはシミュレーションで書いたHaskellの関数が使るという確実なメリットが。

ということでHaskellでロボットが動いたので、後はどなたか関数型言語でロボットを動かす講義をやって欲しい・・・。私はやりません。

参考: Raspberry Pi Mouseって何?

こちらのページで。

Pocket
LINEで送る

日記(SoftwareDesignとシェル芸勉強会)

Pocket
LINEで送る

書いた。SoftwareDesign8月号

数日前、SoftwareDesign8月号が発売されました。

Haskellを担当して書きましたが、どう考えてもその界隈の方々に叱られるような内容。記事を書くときは誰に向けて書いているのかブレないようにしてますが、今回は本当にHaskellを触ったことのない人に階段の一段目を踏んでもらう(あるいは触るべきではない人が触らないようにする)ことを念頭に、本当に簡単なことで、一番本質的だと自分が考えていることを書きました。もちろん、これは自分が本質的だと思っていることで、他の人はそう思ってないかもしれません。異論のある方とも、初心者にはどう接するべきか、一緒に考えてみたいなあとも思います。

ただ、余計な事もいろいろ書いてしまったかなあと思います。面白がってくれる人もいますが、怒る人もいるでしょう。

シェル芸勉強会やりますよ

こっちは本職なので堂々と。8/29(土)にjusさんと共催でシェル芸勉強会を行います。今回から午前の部を設けました。できれば初心者向けにいろんな方々に講習を担当していただければという考えです。そのうち2日構成にでもしましょうか。オーバーナイトセッション夜の耐久シェル芸勉強会とか。死にますね。死にます。

ICRAとりあえず6ページ埋めた

ICRAというのはイクラではなくて、ロボット関係の国際学会で一番重要なヤツです。平たく言えば、夏の甲子園の世界大会みたいなものです。下手な(最近は下手でなくても)論文はバシバシ落とされます。ただ、国際学会なのでそんなに力んでやるもんでもないので、自分の場合は締め切りだいぶ前にさっさと書いて出して他の仕事をするというのが、20代のときからの習慣です。

復帰後連敗街道まっしぐらですが。通らば。実機実験入れたから通して!

ということでICRAで日曜なのに結構物書きをしていた一日でした。もっと英語が速く書けるようになりたい。というか、まだ往年のスピードが戻っていない。SoftwareDesignの方がいろいろ気持ちに引っかかってますが、評価というのは再び仕事が来るかどうかだけです。自分ではコントロールできません。

寝る。

Pocket
LINEで送る

Haskellでコマンドを使えるShellyというものを試したのでメモ

Pocket
LINEで送る

始めてこの分野で論文を書くための調査メモです。はいはい、サーベイサーベイ・・・。

https://hackage.haskell.org/package/shelly

理屈は使ってから考える人間なので、とりあえず理屈抜きで使ってみたので手順を磔の刑に処します。

続きを読む Haskellでコマンドを使えるShellyというものを試したのでメモ

Pocket
LINEで送る

めも

Pocket
LINEで送る

勉強になります。

Real World Haskell の古いところ | あどけない話

Pocket
LINEで送る

SoftwareDesign2月号にHaskellの記事を書いてかなり心臓に悪い。

Pocket
LINEで送る

もうすぐ月曜日ですね・・・。上田です。

SoftwareDesignが土曜日に発売されました。特集は「関数型プログラミング再入門」ですが、この中で私、あろうことかHaskellを担当いたしました。

続きを読む SoftwareDesign2月号にHaskellの記事を書いてかなり心臓に悪い。

Pocket
LINEで送る

Haskell版のcgi-nameとketa

Pocket
LINEで送る

Haskell版のOpen usp Tukubaiのコマンド:cgi-namea.hsketa.hsをリリースしましたー。他にやることが山積みですが。

正月は実家に帰りますが基本、ずっと仕事の模様。

南無阿弥陀仏。

使い方

まずGHCでコンピャイル。

uedamac:COMMANDS.HS ueda$ ghc keta.hs 
[1 of 1] Compiling Main             ( keta.hs, keta.o )
Linking keta ...
uedamac:COMMANDS.HS ueda$ ghc cgi-name.hs 
[1 of 1] Compiling Main             ( cgi-name.hs, cgi-name.o )
Linking cgi-name ...

続きを読む Haskell版のcgi-nameとketa

Pocket
LINEで送る

シェル芸勉強会に対して自ら敢えてHaskellで他流試合を申し込む

Pocket
LINEで送る

昨日のシェル芸勉強会に対して、自分で喧嘩を売ってみます。書きなぐりのHaskellコードです。問題はコチラ↓。

Q1(データのフリップ)

uedamac:tmp ueda$ cat q1.hs
main = getContents >>= putStrLn. unwords . f . words
f [] = []
f (a:b:c) = [b,a] ++ f c
uedamac:tmp ueda$ echo {1..10} | ./q1
2 1 4 3 6 5 8 7 10 9

続きを読む シェル芸勉強会に対して自ら敢えてHaskellで他流試合を申し込む

Pocket
LINEで送る

/dev/randomを利用してHaskellでタネの要らない乱数を作って使う方法考えたがどうだろう?

Pocket
LINEで送る

今,ちょっとしたシミュレーションを行うために,コマンドをHaskellで作ってシェルスクリプトでつなぐということをやっています.シミュレーションでは乱数を多用するのですが,こういう作りだとコマンドをまたいだ乱数が必要です.ただ,コマンドごとに乱数を作るとき,コマンドで毎回タネを設定してしまうと,私の用途ではいい乱数になりません.これはHaskellに限らず,他の言語でも同じ事です.

続きを読む /dev/randomを利用してHaskellでタネの要らない乱数を作って使う方法考えたがどうだろう?

Pocket
LINEで送る

Haskellのerror関数が型破りなんだが型破ってない。

Pocket
LINEで送る

Haskellでコマンドを書いていて、エラーの出し方がよく分からないのでゴマカシゴマカシしていたのですが、真面目にオライリーの本を読んでるとerrorという関数を見つけました。ちゃんと例外処理をやるにはモナド使えとかなんとか書いてありましたが、コマンドならこれが一番いいかと。

例えば、標準入力から文字列を読んで最初の単語だけ出力するプログラムを書きます。

uedamac:~ ueda$ cat hoge.hs
import System.IO

main :: IO ()
main = getContents >>= putStrLn . firstWord

firstWord :: String -> String
firstWord cs = head $ words cs

こいつをコンパイルして実行すると実行できる訳ですが、単語を入力しないという意地悪をするとエラーが発生します。

uedamac:~ ueda$ ghc hoge.hs
[1 of 1] Compiling Main             ( hoge.hs, hoge.o )
Linking hoge ...
uedamac:~ ueda$ echo This is a pen. | ./hoge
This
uedamac:~ ueda$ echo | ./hoge
hoge: Prelude.head: empty list
uedamac:~ ueda$ echo $?
1

そんな意地悪な入力にはちゃんと教育的指導を与えなければいけません。error関数を使うとちゃんと自分でエラーメッセージを作文できます。

uedamac:~ ueda$ cat hoge2.hs 
import System.IO

main :: IO ()
main = getContents >>= putStrLn . firstWord

firstWord :: String -> String
firstWord cs = if (length $ words cs) == 0
               then error "no words"
               else head $ words cs

はい実行。

uedamac:~ ueda$ ghc hoge2.hs
[1 of 1] Compiling Main             ( hoge2.hs, hoge2.o )
Linking hoge2 ...
uedamac:~ ueda$ echo This is a pen. | ./hoge2
This
uedamac:~ ueda$ echo | ./hoge2
hoge2: no words
uedamac:~ ueda$ echo $?
1

終了ステータスは1で固定なんでしょうか?まあ、今の時点ではよいでしょう。

んで、このerror関数を使うなとかなんだとかいろいろ議論はあるんですが、面白いのは型で、こんな定義になってます。

error :: [Char] -> a

出力が任意の型(a)になってるので、型のチェックに通るワケですね・・・。しかし、ずるいことにa型を返すと言いつつこいつは何も返しません。この関数が呼ばれると、他の処理を全部破棄して何も恐れるものの無い状態にしてからエラーを吐くだけなので、処理上、特に問題ないようです。

これ考えた人、これ考えたときどんな顔してたんでしょうね?

いろいろ興味が尽きませんが、寝るです。

Pocket
LINEで送る

HaskellでStringを使う場合とByteStringを使う場合の速度比較

Pocket
LINEで送る

ここ数ヶ月、だれに頼まれたわけでもなく、open usp TukubaiをPython版からHaskell版に置き換える作業をだらだら続けていますが、最近ようやくHaskellを書くのに不自由さを感じなくなってきました。

そうなってくるとスピードのことが気になります。Haskell には文字列を扱う String と、それよりもう少し抽象度の低い ByteString がありますが、スピードは抽象度の低い ByteString の方が速いそうです。しかし、Haskell初心者の脳みそに負担をかけるのは学習速度が遅くなるという個人的な戦略のため、現在リリースされているコマンドでは String を使っていました。しかし、そろそろ挑戦するべきかと。

んで、さっきdelfというコマンド内部で扱う文字列を String から ByteString (Data.ByteString.Lazy.Char8)に変更したので、スピードを計ってみました。delf は、こんなコマンドです。

bsd /home/ueda$ head -n 3 ~/TESTDATA 
2377 高知県 -9,987,759 2001年1月5日
2910 鹿児島県 5,689,492 1992年5月6日
8458 大分県 1,099,824 2010年2月22日
###二列目をdelfで除去###
bsd /home/ueda$ head -n 3 ~/TESTDATA | delf 2
2377 -9,987,759 2001年1月5日
2910 5,689,492 1992年5月6日
8458 1,099,824 2010年2月22日

便利でしょ?え?使いみちが分からない?分かれば便利なんです。

delfが便利かどうかはおいておいて、String版(delf.normal)とByteString版(delf.bs)を作ったので、比較してみます。ByteString 版はココにアップしてあります。

###python版###
bsd /home/ueda/tmp$ time head -n 100000 ~/TESTDATA | delf 2 > /dev/null

real	0m3.804s
user	0m3.814s
sys	0m0.031s
###string版###
bsd /home/ueda/tmp$ time head -n 100000 ~/TESTDATA | ./delf.normal 2 > /dev/null

real	0m1.661s
user	0m1.627s
sys	0m0.079s
###ByteString版###
bsd /home/ueda/tmp$ time head -n 100000 ~/TESTDATA | ./delf.bs 2 > /dev/null

real	0m0.773s
user	0m0.741s
sys	0m0.083s

おー。二倍以上。

これは仕事用ハイスペックマッシーンでやったらもっと速いに違いない。・・・ということで仕事用のサーバ(CentOS5.9)で try。

[usp@demo1 ueda]$ uname -a
Linux demo1 2.6.18-308.8.1.el5 #1 SMP Tue May 29 14:57:25 EDT 2012 x86_64 x86_64 x86_64 GNU/Linux
###有償ビジネス版###
[usp@demo1 ueda]$ time head -n 1000000 TESTDATA | delf 2 > /dev/null

real	0m0.448s
user	0m0.482s
sys	0m0.085s
###String版###
[usp@demo1 ueda]$ time head -n 1000000 TESTDATA | ./delf.normal 2 > /dev/null

real	0m5.465s
user	0m5.476s
sys	0m0.096s
###ByteString版###
[usp@demo1 ueda]$ time head -n 1000000 TESTDATA | ./delf.bs 2 > /dev/null

real	0m13.397s
user	0m13.406s
sys	0m0.075s

あれれれれ????遅いやんけ!!!何回やっても遅い。

うーん。終わり。

appendix: 個人メモ

bsdなら一発コンパイルだったのに、CentOS 5.9でコンパイルしようと思ったらめちゃくちゃ叱られたので、以下のようにコンパイル。
なんか遅い原因はここらへんに関係するのか?どうなのか?

[usp@demo1 ueda]$ cat /etc/redhat-release 
CentOS release 5.9 (Final)
[root@demo1 ~]# cabal install parsec
...
[usp@demo1 ueda]$ ghc delf.bs.hs 
[usp@demo1 ueda]$ ghc --make -o delf.normal delf.normal.hs
Linking delf.normal ...
Pocket
LINEで送る