haskellのproの技を鑑賞
VRで、オイラーの20年後(それまで生きていればですが)を体験してる。そんなゴーグルの お化けみたいのを付けているんか?
いえ違うってば。魔女の一撃に遭ったのさ。布団を上げようとして ぎくり。
今回の一撃は、人生で一番強烈なやつ。今までは、年寄りくさくなるってんで、膏薬の 類は拒否してたんだけど、今回は試してみた。(背に腹は代えられないからね)
サロンパスとトクホンのABテストですよ。幸い両方とも家に有ったからね。女房が足が痛い、 腕が腱鞘炎ぽいとか言って使ってたんだ。
どちらがオイラーにマッチしたかと言うと、トクホンかな。剥がす時にちょっと痛いけど、 それだけきちんと肌に密着してるからね。寝てて、苦労しながら寝がえりを打つと、サロンパス だと、よれよれになってしまう。トクホンはそんな事無し。
自立して歩けない。壁をつたわったり、手摺につかまったりしてそろりそろりと歩いている。 痛い所をかばうので、体幹が曲がっている。
gnuplotで関数のグラフ描き
前回、方程式の求解に因数分解やmaximaの関数を使うってのをやった。その時、だめなら グラフでも書いてみればいいじゃんと、うっちゃっておいた。どうやるか、調べてみた。
グラフ描きなら、pythonでmatplotlibでしょは、pythonかぶれなんでやらない。古式ゆかしく gnuplotです。
で、gnuplotもリナやFreeBSDに登録されてるやつは、Qtが必要とか、LaTeXが付いてきたりで 大がかり過ぎるので、単純明快にOpenBSDのやつを入れてあげた。
gnuplot> set xrange [-2:2] gnuplot> set grid gnuplot> f(x) = x**2 - 3 gnuplot> plot f(x) gnuplot> plot [-4:4] f(x)
描画したい関数を登録。Xのスキャン範囲を指定。そして描画ってステップ。グリッドを表示 しておくと便利。最後の例みたいに、範囲と描画を同時に指定が出来るので、便利。
gnuplotの使い方を調べていて、棒に当たったので、記録しとく。
gnuplot の基本的なテクニック - フーリエ展開の可視化をやってみる
gnuplot> f(x) = 3*x**2 - 7*x - 23 gnuplot> plot [-5:5] f(x)
これ、前回、因数分解に失敗したやつ。グラフを書いてみると、下に凸の二次曲線。極小値は、 x=1.15ぐらいでyは-27ぐらいか。根は、-1.8と4.1ぐらいと目測。見える化の威力だな。 オイラーが学生の頃、こんなグラフを書こうとすると、ねじり鉢巻きで、計算尺をつかってたな。今は、一瞬。
(%i7) f(x) := 3*x^2 - 7*x -23; 2 (%o7) f(x) := 3 x - 7 x - 23 (%i9) find_root(f, -2, 0); (%o9) - 1.837959396219991 (%i10) find_root(f, 0, 5); (%o10) 4.171292729553325
一応、検算しとくか。
(%i12) solve(f(x),x); 5 sqrt(13) - 7 5 sqrt(13) + 7 (%o12) [x = - --------------, x = --------------] 6 6
ありゃ、無理数はそのままなのね。数値に直してみる。
(%i15) (5 * float(sqrt(13)) - 7) / 6; (%o15) 1.837959396219991 (%i16) (5 * float(sqrt(13)) + 7) / 6; (%o16) 4.171292729553325
合ってた。よかった。
今度は、下に凸(世間一般には、谷と言うのかな)の、頂きを求めてみるか。 元の式を微分して、導関数を求め、その式を解けばいいのだな。曲線が減少してる時は、 接線が傾きが負。曲線が増加してる時は、接線の傾きが正になる。負->ZERO->正の、ZERO点を 求めると、頂きになるんだな。
(%i13) solve( diff(f(x),x)=0, x); 7 (%o13) [x = -] 6 (%i14) 7.0 / 6; (%o14) 1.166666666666667
答えは有理数で返ってくるとは、schemeみたいな奴だな。
(%i18) find_root(f, -2, 6 ); find_root: function has same sign at endpoints: f(- 2.0) = 3.0, f(6.0) = 43.0 -- an error. To debug this try: debugmode(true);
残念ながら、一度に2つの根は求められないようだ。始点と終点の符号が異なっている事を 要求してる。(ZEROを過る範囲を与えるのは、ユーザーの責任範囲とな)
どんな風にやってる? /usr/local/share/maxima/5.39.0/src/intpol.lispがそれっぽい。
(defun find-root-subr (f left right &key (abserr maxima::$find_root_abs) (relerr maxima::$find_root_rel)) : (let ((lin 0) (a left) (b right) (fa (convert (funcall f (maxima::to left)))) (fb (convert (funcall f (maxima::to right)))) c fc) : (when (plusp (* (float-sign fa) (float-sign fb))) (if (eq maxima::$find_root_error t) (maxima::merror (intl:gettext "find_root: function has same sign at\ endpoints: ~M, ~M") `((maxima::mequal) ((f) ,a) ,fa) `((maxima::mequal) ((f) ,b) ,fb)) (return-from find-root-subr 'maxima::$find_root_error))) : ;; Use binary search to close in on the root (loop while (< lin 3) do (setq c (* 0.5 (+ a b)) fc (convert (funcall f (maxima::to c)))) (unless (numberp fc) (return-from find-root-subr (values nil a b))) (when (interpolate-check a c b fc abserr relerr) (return-from find-root-subr c)) (if (< (abs (- fc (* 0.5 (+ fa fb)))) (* 0.1 (- fb fa))) (incf lin) (setq lin 0)) (if (plusp fc) (setq fb fc b c) (setq fa fc a c)))
CommonLisp(Lisp-2)のいやらしさが、そこはかとなく漂っているけど、やろうとしてる事は 単純。与えられた区間 left,rightの間で、関数fの値の正負が反転してる事を確認する。 問題無かったら、バイナリーサーチでZERO点を探す。(ここには載せていなけど、この後、 補完法を使って、更に値を精密化してる。
maximaでグラフを書く
説明書によると、gnuplotをグラフ職人として雇っているとか。勿論その職人が気にいらなければ他の職人を使う事が出来るそうな。そんな訳で、gnuplotをドライブするモジュールが、 maximaとは別になってるそうだ。ならば、そのモジュールを(Lisp用語だとパッケージ になるけど)、探してみる。
$ pwd /usr/local/share/maxima/5.39.0/share/draw $ ls draw-index.lisp drawutils.mac picture.lisp worldmap.mac draw.lisp drawutils.texi texinfo.tex draw.system gnuplot.lisp vtk.lisp draw_gnuplot.dem grcommon.lisp wbd.lisp
デモと言うか今風に言うと、プレゼン用のスクリプト(台本)が有るので開いてみると、 これで走り出すようだ。
(%i1) demo ("draw_gnuplot.dem") $ read and interpret file: /usr/local/share/maxima/5.39.0/share/draw/draw_gnuplot.dem At the '_' prompt, type ';' and <enter> to get next demonstration. (%i2) load("draw") _; (%i3) "Axes with different styles" _; (%i4) draw2d(explicit(x^3,x,-1,1),xaxis = true,xaxis_color = blue, yaxis = true,yaxis_width = 2,yaxis_type = solid, yaxis_color = "#f3b507") _
ふむ、;セミコロンとRETで、舞台が進行してくんだな。いやと言う程、堪能させてもらい ましたよ。
普通に使う場合は、下記のようにする。
(%i1) plot2d( sin(x)/x, [x,-10,10] ); plot2d: expression evaluates to non-numeric value somewhere in plotting range. (%o1) [/tmp/maxout41195.gnuplot_pipes]
表示に必要なデータは、パイプ経由でgnuplotに渡されるんだけど、ログにも残されていた。 一体どのぐらいのデータが渡るか調べたら、658行の滑らかさだったよ。
cat.hs の解析
前回やったcatをhaskellで実装するってのの追試と言うか、疑問解消に手を染める。 ghciにロードして、型を眺めている。
*Main> :bro main :: IO () type FilterProgram = String -> String runProg :: FilterProgram -> String -> IO () genProg :: Options -> FilterProgram progFusioner :: Options -> FilterProgram -> FilterProgram delete :: Eq a => a -> [a] -> [a] lineCount :: FilterProgram tabConv :: FilterProgram lineSepConv :: FilterProgram helpMessage :: FilterProgram getInputs :: Options -> IO [String] isFileName :: Option -> Bool getFileNames :: Options -> [String] parseOpt :: [String] -> Options data Option = CFlag | TFlag | EFlag | HFlag | File String type Options = [Option]
genProgがどう動くか実験。
*Main> genProg [CFlag] <interactive>:3:1: error: * No instance for (Show FilterProgram) arising from a use of `print' (maybe you haven't applied a function to enough arguments?) * In a stmt of an interactive GHCi command: print it
怒られた。ちゃんと入力を与えているんだけど、そんな関数を表示しようなんて、無謀な事は 出来ませんとな。でも、適切な引数を与えてみてって助言があった。
*Main> genProg [CFlag] "aaa\n\nbbb\n" "1 aaa\n2 \n3 bbb\n" *Main> putStr $ genProg [CFlag] "aaa\n\nbbb\n" 1 aaa 2 3 bbb *Main> putStr $ genProg [CFlag,EFlag] "aaa\n\nbbb\n" 1 aaa$ 2 $ 3 bbb$
ふむふむ、なる程。この関数は、文字列の加工業なのね。どんな加工はするかは、optionsで 指示するんだな。
次に疑問なのは、ここに使われてるid。
genProg :: Options -> FilterProgram genProg opts = progFusioner opts id
説明では、
あとはgenProgか.とりあえずこいつは最初はオプション無視のidでしょ. genProg :: Options -> FilterProgram genProg = const id これでとりあえずcatになるんじゃね? : genProg opts = if elem CFlag opts then ccat else id : なぜidかっていうと,String -> Stringな型でcatのデフォの動作を表現しているのがidだからですよ.念の為.
鈍いオイラーには、まだピンとこない。ええい、idを取り除いてどんな文句を言われるか 観察しよう。
cat.hs:18:16: error: * Couldn't match type `String -> String' with `[Char]' Expected type: FilterProgram Actual type: FilterProgram -> FilterProgram * Probable cause: `progFusioner' is applied to too few arguments In the expression: progFusioner opts In an equation for `genProg': genProg opts = progFusioner opts
フィルターした結果(の文字列)が欲しいって事か。同じシチェーションを作ってみる。
*Main> x = lineCount . tabConv *Main> :t x x :: String -> String *Main> putStr $ x <interactive>:8:10: error: * Couldn't match type `String -> String' with `[Char]' Expected type: String Actual type: String -> String * Probable cause: `x' is applied to too few arguments In the second argument of `($)', namely `x' In the expression: putStr $ x In an equation for `it': it = putStr $ x
なる程、繋がりがあるから大事にせいとな。繋がりのデータがidになってるんだな。 実験コードだと、putStrで文字列を表示してください。だから、文字列が必要。その 文字列は、合成関数でフィルターしたもの。そして、合成関数が仕事をするには、元データと 言うか、文字列が必要。、、、だけど、それを与えなかったんで文句を言われてしまった。
これと同じ事が、コンパイル時にも行われていた。コンパイル時には、具体的な文字列が 無いんで、idで代用? 型の連鎖から、idには文字列が来る事が判明してるとな。
こういう、想像をコードを書く時点でやれってのは、慣れていないと難しいな。
haskell 8.2.1 + OpenBSD
haskell.orgの姉妹サイトであるhttps://haskell-lang.org/の、ドキュメントの部を見てたんだ。そしたら、haskellに関する事が要領よく載ってる サイトが出てた。 WHAT I WISH I KNEW WHEN LEARNING HASKELL
つらつら流し見してたら、ghciのコマンドの説明が出てた。:e で、editorを呼び出せるとな。 以前にもどんなコマンドが有るか調べたはずなんだけど、見落としていた。これ便利そう。 2ストロークでeditorを呼び出せるなら、emacsでの開発環境、ghc-modとさして変わらないじゃん。
だったら、今の所ghc-modを入れるのが絶望的(以前トライして負けた)な、OpenBSDに適用してみようかな。ewwがさっと呼べないのがちょっと悔しいけど、人様のコードをいじくるには 十分だろう。
前提条件として、環境変数EDITORにemacsを設定しておく。そして、やおら
$ ghci cat.hs GHCi, version 8.2.1: http://www.haskell.org/ghc/ :? for help [1 of 1] Compiling Main ( cat.hs, interpreted ) Ok, 1 module loaded. *Main> :e
これで場面が変わってemacsになる。emacsも一度cacheに載ってしまえば、切り替えは非常に スムース、ストレスになる事は無い。そして、適当に編集して、セーブ後、emacsを終了すると、編集結果がghciに再読み込みされる。
[1 of 1] Compiling Main ( cat.hs, interpreted ) cat.hs:25:9: error: * Data constructor not in scope: UFlag :: Option * Perhaps you meant one of these: `TFlag' (line 73), `EFlag' (line 73), `CFlag' (line 73) | 25 | | elem UFlag opts = progFusioner (delete UFlag opts) (toU . f) | ^^^^^ : Failed, 0 modules loaded. Prelude> :e
こうやって、エラーが無くなり、ロードされるまで、モグラたたき(無料)を楽しめます。
上記は機能追加で、全て大文字にして表示しようと言う、昔々のタイプライターを実現しようと 思ったのだ。フラグの追加は都合3か所あるんだけど、宣言を見落としていてエラーになったのんだ。
で、関数 toUが実体になるんだけど、文字列を大文字にするやつ、名前はtoUpperって覚えてるんだけど、どこのモジュールかは、土地勘が無くて知らない。hoogleで聞いてみると、
toUpper Packages base text toUpper :: Char -> Char base Data.Char Convert a letter to the corresponding upper-case letter, if any. Any other character is returned unchanged. toUpper :: Text -> Text text Data.Text O(n) Convert a string to upper case, using simple case conversion. The result string may be longer than the input string. toUpper :: Text -> Text text Data.Text.Lazy O(n) Convert a string to upper case, using simple case conversion. The result string may be longer than the input string.
お望みのものは、baseパッケージとtextパッケージに見つかりましたとな。textパッケージの 方は、ずばり文字列を変換してくれるとな。baseパッケージの方にあるやつを使いたかったら、 Data.Char をincludeしてから使えとな。今回は、こちらでいいな。
import Data.Char toU :: FilterProgram toU [] = "" toU (c:cs) = toUpper c : toU cs
Data.Textを使おうとすると(stackからtextってパッケージを入れて)面倒な事になる。
[fb11: tmp]$ stack ghci Configuring GHCi with the following packages: GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /tmp/ghci986/ghci-script Prelude> :m Data.Text Prelude Data.Text> toUpper "hoge Fuga" <interactive>:2:9: error: * Couldn't match expected type `Text' with actual type `[Char]' * In the first argument of `toUpper', namely `"hoge Fuga"' In the expression: toUpper "hoge Fuga" In an equation for `it': it = toUpper "hoge Fuga"
[fb11: tmp]$ cat aa.hs {-# LANGUAGE OverloadedStrings #-} import qualified Data.Text as ST hoge :: ST.Text hoge = "abc def XYz"
[fb11: tmp]$ stack ghci aa.hs *Main> ST.toUpper hoge "ABC DEF XYZ"
話を開発環境に戻すと、 もう一つのやり方もあるな。emacs cat.hs とかして開き、その画面を C-x 2 して上下に 分割。その片方で、M-x eshell して、shellを走らせ、そこからghciを起動するって方法。 但し、この方法だと、ghciが持ってる補完が効かない(TABをeshellが喰っちゃって、ghciまで 届かないんだろう)という欠点が有る。
Uum
下記に列挙したの、日本のHaskell界の神様、nobsunの投稿を集めたもの。知恵熱が出そうな ものばかりが並んでる。
etc
Haskellでコマンドラインアプリケーションを作る時の基本的な情報とTips