Haskell勉強会準備室2(純粋な型、不純な型(IO))

前回はmecab関連の手製の関数の説明の際に、
型についての基本的な勉強をして終わりました。
今回は上記関数の型に戻ります。

純粋な型、不純な型(IO)

前回の復習等

getMecabed :: String -> IO [String]

前回分だけでもある程度、上の意味は理解出来ると思います。
まずgetMecabedはStringを受け取ってIO[String]を返す「関数」です。
IO[String]はIOを除けばStringのリストになります。
今回は、前回に出てこなかったIOについて簡単な説明をします。
以下をREPLに打ち込んでみてください(=<<ってなんだとかは、おいおいご説明します)。

uprint =<< getMecabed "Haskellはプログラミング初心者向け言語である。"

VSCodeのREPLはまずターミナルをShift+Ctrl+@で立ち上げた後に、

stack ghci

と打ちます。これでREPLが立ち上がります。
この状況で上のuprintを打ち込むと、

*Main Lib> uprint =<< getMecabed testStr
["今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー","は\t助詞,係助詞,*,*,*,*,は,ハ,ワ","Haskell\t名詞,固有名詞,組織,*,*,*,*","の\t助詞,連体化,*,*,*,*,の,ノ,ノ","勉強\t名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー","会\t名詞,接尾,一般,*,*,*,会,カイ,カイ","です\t助動詞,*,*,*,特殊・デス,基本形,です,デス,デス","。\t記号,句点,*,*,*,*,。,。,。","EOS"]

確かにリストが出力されているのが分かります。
もう少し分かりやすく表示すると、  

*Main Lib> uprint =<< getMecabed testStr
[  "今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"
 , "は\t助詞,係助詞,*,*,*,*,は,ハ,ワ"
 , "Haskell\t名詞,固有名詞,組織,*,*,*,*"
 , "の\t助詞,連体化,*,*,*,*,の,ノ,ノ"
 , "勉強\t名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー"
 , "会\t名詞,接尾,一般,*,*,*,会,カイ,カイ"
 , "です\t助動詞,*,*,*,特殊・デス,基本形,です,デス,デス"
 , "。\t記号,句点,*,*,*,*,。,。,。","EOS"]

リストになっていますね。

(<-)について

さらに以下をVSCodeのREPLに入力してみましょう。

testStr = "今日はHaskellの勉強会です。"
result = getMecabed testStr
result2 <- getMecabed testStr

resultとresult2の型をチェックしましょう。型チェックは:tです。

*Main Lib> :t result
result :: IO [String]
*Main Lib> :t result2
result2 :: [String]

いかがでしょうか。resultはIOがついていますが、result2はIOが消えています。
両者の違いは(=)か、(<-)かです。他言語だとこれらは代入と言われたりします。
さらにそれら言語だとこれら2つを区別しないです。Haskellはこれら2つを明確に区別します。
そしてその区別は非常に重要です。勉強会が進めばわかってくると思います
Haskellだと後者は代入ではなく束縛(binding)と呼ばれたりします。
前者は代入というよりは定義です。resultはgetMecabed testStrと定義される。
他の箇所でresultが出てきたら、定義上getMecabed testStrで置き換え可能である、といった具合です。
今は詳細な説明はおいておきます。まずはHaskellのコードを書くのに慣れましょう。

IOに関する現段階での押さえておくべきポイント

IOが出てきた場合に、現段階で押さえておくべきポイントは以下の数点です。

IOを消したい場合は、(<-)を使うこと

IOが出てくると色々と困ると思います。やってみると分かりますが、普通の関数を普通には適用できなくなります。
これが中々にHaskell初心者殺しです。IOまみれで困ったら(<-)でこれらを消していきます。
これを知っておくだけでも、気持ちがだいぶ楽になりますし、悩む時間が短縮されます。

IOは極力避けること

IOがつかなくて済むやり方があるなら、そちらを必ず採用してください。
下でも出てきますが、IOは不純、IOがついていないのは純粋、などと呼ばれます。
これがなかなかに良い譬えで、一度IOに染まると純粋になることは出来ません。
そういう潔癖さを習慣化する、のが大事です。やってみると分かりますが、
IOを多用すると、色々とHaskellに特有の構文をたくさん駆使する必要が出てきます。
既に上の(<-)もそうですね。こういう構文は重要なのですが、極力避けたくなります。
そしてその直観と戦略はHaskell的に間違っていないです。純粋な気持ちを忘れないでください。
(もちろん敢えてそういう特殊構文を使うメリットはあります。が、初心者は最初は避けときましょう。)

もう少し込み入ったIOの説明

プログラミングを経験している人向けに、もう少し踏み込んだIOの説明をします。
初心者は飛ばし読みで良いです。後で気になったら再度読んでみてください
ライブラリのインポートの仕方については現段階でも大事なので、そこはじっくり読んで実行してみてください。
HaskellのIOについての、ざっくりとした直観です。
IOはInput Outputの略です。普通はファイル入出力とかで使われますが、HaskellだとHaskellの内外みたいな扱いになります。
Haskellの内側の純粋な世界(pure)と、外の不純な世界(impure)。その間をつなぐInput Outputみたいな表現がなされたりします。
じゃあHaskellとか純粋ってなんだ?という感じですが、
純粋というのは、 それを評価した際に、Haskellプログラム側から見て結果がすべて把捉出来るもの、です。
(勉強会ではココに図を書いておく)
それを評価の「それ」というのはHaskellの式になります。以下のようなものです。

1
"これは式です。"
1 + 2
getMecabed "これは式です。"

REPLに入力した式、みたいに思っておけばいいです。
REPLのEはEval(評価)でした。
REPLにHaskellの式を入力した後で、キーボードのエンターを押す行為がEvalに相当します
純粋、つまり評価時にHaskellが結果を全て把捉出来る例として、結果が一意に定まるという表現もなされます。
一意にというのは、いつも同じ結果になるという意味になります。
細かいこと言うと、一意であってもHaskellが把握できない要素(副作用)がある場合は純粋ではないです、が、
ここでは込み入ってくるので、副作用についてはおいておきます。
ちなみに数学の関数は入力に対して結果が一意に定まります。
ある入力xに対して2つの値が対応したりしません。必ず一つです。
ある入力xに対して2つ以上対応する場合は関数(function)ではなく、関係(relation)になってきます。
Haskellの中でも数学的な領域が純粋という直観は間違っていないです。
前回に出てきた関数、

readInteger :: String -> Int
toUpperString :: String -> String

はいずれも純粋な領域だけを扱っています。
これらは関数なので純粋関数と呼ばれます。
純粋関数は入力に対して結果を一意に返します。
readIntegerは例えば'0'を入力すれば必ず0が出力されます。
toUpperStringに"hello"を入力すれば必ず"HELLO"が出力されます。
これが不純な関数の場合は、

readInteger' :: String -> IO Int
toUpperString' :: String -> IO String

のように出力される型にIOがついてきます。
それでは不純な関数、すなわち同じ入力をしたとしても、時と場合によって違う出力がなされる関数って何があるかとなります。
一例はまさに時と場合、すなわち時刻を返す関数です。
Haskellだと例えばgetZonedTimeです。これを使うにはData.Timeというライブラリをインポートする必要があります。

ライブラリのインポートの仕方

以下を、ソースコードの冒頭、他のimport文が並んでいるところに追加してください。

import           Data.Time

VSCodeだと警告が出てくると思います。電球みたいのが出てくるので、押してみると、
Add time as a dependencyなどという候補が幾つか挙げられています。
これは、今開いているプロジェクト、つまり私がzipファイルで用意したnlpというプロジェクトは、
Data.Timeというライブラリに依存(depend)していないということになります。
依存させるには、package.yamlというファイルに追記する必要があるのですが、
それをVSCode(正確にはHIE)が勝手にやってくれる感じです。
これは凄いことです。まず問題を解決する手段を幾つか探してきて、それを選択したら直ぐに実行してくれます。
一応、package.yamlのどこに追記されたかを見ておきます。

library:
  source-dirs: src
tests:
  nlps-test:
    source-dirs: test
    main: Spec.hs
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - unicode-show
    - foldl
    - nlps
    - vector
    - text
    - turtle
copyright: 2020 Author name here
maintainer: example@example.com
dependencies:
- time <- ★これがVSCode(HIE)により追記された
- unicode-show
- foldl
- base >= 4.7 && < 5
- vector
- text
- turtle

それではbuildしてみましょう。HaskellのREPLから抜けて通常のターミナルに戻すには、

:q

と打ちます。qはquitの略です。
通常のターミナルに戻ったら、

stack build

してみてください。これで、Data.Timeへの依存が無事行われれます。
依存という直訳って何か普通の日本語でないですね。
Data.Timeライブラリが関連付けられます、辺りの意だと思います。
buildしたあとに再度REPLを立ち上げます。stack ghciです。

もう少し込み入ったIOの説明(つづき)

その後、getZonedTimeと打ち込むと、以下のような結果が出力されます。

*Main Lib> getZonedTime
2020-02-01 22:26:12.234479067 JST

少し時間を空けてもう一度やってみましょう。
もう一度手でgetZonedTimeと打ってもいいですが、
ターミナルでは↑ボタンを押すと前回入力した内容が出てきます。何度も↑を押すと更に遡れます。
うまく利用していきましょう。再度やると、以下のような結果になります。

*Main Lib> getZonedTime
2020-02-01 22:30:24.572583585 JST

ここで注目すべきは、結果が変わっていることです。
getZonedTimeの型を確認しましょう。何になるか予想できますか?

*Main Lib> :t getZonedTime
getZonedTime :: IO ZonedTime

予想通りIOがついていますね。
このように結果(正確にはそれが評価された際の結果)が毎回異なる、
こういうものにIOという型修飾がかかってきます。

getMecabedがIOな理由

さて、最初に戻ります。
じゃあなんでgetMecabedはIOが付いているのか、本当に不純な関数なのか。
実際、getMecabedに入力する文章が同じであれば、同じ結果がいつも返ってきます。
なので、getMecabedも純粋関数な感がありますが、違います。
理由は、 MecabがHaskellの外のライブラリ だからです。
Mecabと同様のものをHaskellでつくった場合は純粋関数になります。

純粋関数が重要な理由

さっきから純粋関数が重要、不純は避けるべきと言っていますが、
一つの理由は計算の最適化が絡んできます。
結果が同じになる関数、Haskell側からみてそうなると把握できる関数は、
数学的な最適化の対象になってきます。その辺り、極力すべてHaskellで書くべき、
というモチベーションになってきます。
ただし、現状では私の体験した範囲ではHaskellの最適化は不十分な感じです。
学者さんたちが頑張っていますが、まだ余地が大きくあります。
量子コンピューティングあたりがそのブレイクスルーになる気がしています。
現状では速度を求める場合には、高速で動くCのライブラリをHaskell側から読んで使う、
というやり方を採ることになりますが、最適化技術がこなれた将来には全てHaskellで書く、
みたいな事になるかも知れません。
そろそろ長くなってきたので、今回はこれくらいにしておきます。
IOについては、実際の事例(ファイルの入出力とか、GUIとか色々あります)をたくさん経験するとだんだん分かってきますので、
今、上の説明でわからなくても問題なしです。後で戻ってもう一度復習してみてください。

メモ

なかなかgetMecabedから進みませんね、、
初心者の人に説明する場合には、モチベーションが下がらないように途中の詳細説明をうまく省く必要がありそうです。
getMecabedに色々と入力して遊ぶところで終わっておいたほうが良い気もしてます。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする