Haskell勉強会準備室1(Mecabの導入と型)

VSCodeで勉強会を進めることになったので、VSCodeにある程度慣れてから第二回を開こうと思っています。
目標としては2月下旬から3月上旬にしたいと思っています
デバッグとかSpacemacsの時はRepl(後述)を多用していたのですが、VSCodeだとphoityne-vscode
みたいに、Visual Studioでブレークポイントを張ってデバッグみたいなのが出来そうなので、
その方式も試しておきたいです。
なのでひとまず第二回で扱うネタをVSCodeで色々やって見ようと思います。ここである程度は予め公開していきます。
具合が良さそうなものをチョイスして勉強会でご紹介する予定です。

Mecabのインストール

[Mecab](https://taku910.github.io/mecab/)
を導入します。

Ubuntuの場合

以下はUbuntuでのやり方です。Macもそうは変わらないはずです。

Mecab本体

ターミナルに下のコマンドを打ち込んでください。

sudo apt install mecab
sudo apt install libmecab-dev
sudo apt install mecab-ipadic-utf8

辞書ファイル

ファイルが違うだけでやり方はMecab本体と同様です。
ソース(mecab-ipadic-2.7.0-20070801.tar.gzをダウンロードして、
ホームディレクトリ(/home/ユーザ名)においてください。
その後で以下をターミナルに打ち込んでください。

tar zxfv mecab-ipadic-2.7.0-20070801.tar.gz
cd mecab-ipadic-2.7.0-20070801.tar.gz
./configure -with-charset=utf8
sudo make install

Windowsの場合

公式のBinary-Package for MS-Windows (mecab-0.996.exeのようなの)をダウンロードして実行してください。
これによりProgramFiles直下にMecabというフォルダが出来るはずです。その中のbinフォルダにPathを通しておいてください。
これはHaskell側でMecabを呼べるようにする設定です。

手製の関数(天下り的)

Mecabで遊ぶ用の関数セットを幾つか私の方で用意しました。

このファイルzipファイルです。以下、このプロジェクトフォルダで遊んでいきます。
解凍したフォルダをVSCodeで開いて使っていく感じです。
gitの方が分かる人向けはこちら
最初のうちはこれら関数は天下り的に使ってみてください。
何をやっているかは勉強会が進んだら説明します。
しかし、VSCode、すごいですね。これらの関数は以前の個人的な開発でつくったものですが、
勉強会向けのソースにコピペすると、足りないパッケージを提案してきてくれます。
他にもLinterが効いて、綺麗なコードを提案してくれます。
この辺りの提案は関数型プログラミング言語だからこそゴリゴリやってくれるのでしょう。
このLinterのポテンシャルが大きいのがHaskellです。
綺麗なだけでなく、速いコードも提案してくれる、今でも一部は提案していますがその種類が増える、
そう期待されていますし、学者さん達が頑張っている感じです。

最初の一歩 -型について

getMecabedという関数を用意しました。以下の感じです。

getMecabed :: String -> IO [String]
getMecabed sens = do
  let
    cmd = fromString $ "echo " P.++ "\""  P.++ sens P.++ "\"" P.++ " | mecab"
  getShellRes cmd

ここから、上の関数について解説していきますが、その前に一度上の関数を動かしておきましょう。
VSCodeでターミナルを呼び出して、

stack ghci

と打ち込んでください。これで、今開いているHaskellのソースファイル(.hsファイル)が読み込まれます。

*Main Lib> 

みたいな状態になっているはずです。試しに

*Main Lib> 1 + 2

とかを入力してみてください。簡単な電卓として使えます。電卓以上のことも当然出来ます。
これをREPLと呼びます。Read Eval Print Loopの略です。
ファイルを読みこみ(Read)、ファイルの内容を評価し(Eval)、結果を表示(Print)し、それがずっと続く(Loop)といった具合でしょうか。
明示的に脱出しない限り、1+2を評価した後に2+3を評価できるようにループしています。
Haskellのデバッグ方法の一つはREPLを用いたものになります。この勉強会でもREPLを使って遊んでいきます。
さて、Mecabの関数に戻ります。
REPL化したターミナルに、以下を打ち込んでみましょう。

getMecabedShow "これはテストです。"

すると、REPLに以下のような表示がなされるはずです。

(0,"これ\t名詞,代名詞,一般,*,*,*,これ,コレ,コレ")
(1,"は\t助詞,係助詞,*,*,*,*,は,ハ,ワ")
(2,"テスト\t名詞,サ変接続,*,*,*,*,テスト,テスト,テスト")
(3,"です\t助動詞,*,*,*,特殊・デス,基本形,です,デス,デス")
(4,"EOS")
[(),(),(),(),()]

いかがでしょうか。Mecabはこのように日本語を文法的に解析してくれるソフトになります。
すごいソフトです。インターネットの文章とかをコピペして入力して遊んでみると面白いです。
以下、Haskellっぽい説明をしていきます。

getMecabed :: String -> IO [String]
getMecabed sens = do
  let
    cmd = fromString $ "echo " P.++ "\""  P.++ sens P.++ "\"" P.++ " | mecab"
  getShellRes cmd

二行目以降は今はとりあえずおいておきます。
注目すべきは一行目です。即ち、

getMecabed :: String -> IO [String]

ここになります。これは型宣言と呼ばれる箇所で、
getMecabedという関数がどのような型を持っているかを説明しています。
型というのは初回にも説明しましたが、種類みたいなものと思ってください。
この勉強会でよく出てくる型の例を挙げてみます。

Int : (整数型)
Float : (実数型)
Double : (実数型)
Char : (文字型)
String : (文字列型)
[a] : (aという型のリスト型)
IO a : (aという型のIO型)

上から説明します。
IntはIntegerの略で整数を表します。
-10, -9, … 0, 1, 2, … 100 …
みたいなやつです。
これに対し、FloatやDoubleは実数を表します。
Charは文字です。英語であれば、a, b, c, … z, @, % .. などといった感じです。
日本語であれば、 あ、い、う、え、お、か、、、、日、月、火、、、1、2、など、ひらがな、カタカナ、漢字、数字、記号などがChar型を持つ例になります。
共通するのは 文字が一つ ということです。これに対し、
Stringは文字列型になります。
文字列は文字が複数並んでいるものになります。「これは」、「テスト」、「です」、「。」などが文字列です。
最後の「。」は文字が一つなのですが、これは文字列にもなります。正確には文字としての「。」と文字列としての「。」を区別します。
Haskellに限らず、プログラミング言語だと以下のような使い分けになっていると思います。

文字としての  。  '。'
文字列としての。  "。"

いかがでしょうか。文字の場合は一つの引用符「’」(日本だとダッシュ、海外だとプライムって言ったりします。プログラミングだとシングルクオートと呼びます。)、文字列の場合は2つの引用符「"」(ダブルクオート)で囲うことになります。
これらはHaskellでは区別されます。言語によってはこれらは区別されません。Haskellは堅苦しい(型苦しい)言語なので、ガチガチにこの辺りは区別していきます。
もうひとつ型がガチガチな例を述べます。下の書き方はHaskellだと怒られます。ビルドが通りません。

n2d = 2.0 :: Double
n3d = 3.0 :: Double
n2i = 2 :: Int
n3i = 3 :: Int
n2d / n3d
n2i / n3i

ちょっとトリッキーなことをしていますが、とりあえずREPLにコピペして見てください。
心としては、

2.0 / 3.0 <- これはO.K.
2 / 3 <- これはN.G.

こういうのを示そうとしています。代入するとそうなっています。
さて、この2つは同じく三分の二なのですが、前者はO.K.で後者はN.G.です。
これはこの割り算(/)は実数型(Float,Double.2.0や3.0はFloatやDouble型)では定義されているが、整数型(Int.2や3はInt型)には定義されていない
ということでエラーになります。
以下のようなエラー文が出てくるはずです。

<interactive>:18:1: error:
    • No instance for (Fractional Int) arising from a use of ‘/’
    • In the expression: n2i / n3i
      In an equation for ‘it’: it = n2i / n3i

こういう融通の効かなさが最初のうちは腹立たしく感じるかも知れませんが、逆にバグを回避してくれます。
同様の状況でC++ではコンパイルエラーを出さない、しかし意図していない挙動(値が0になる等)になるような経験をしたことがあります。
Haskellだとそういうバグが出る余地はないです。
Mecab関連の関数に戻ります。

getMecabed :: String -> IO [String]

上で説明していない要素として矢印(->)が出てきています。
これは関数を表します。Haskellは純粋関数型プログラミング言語です。
関数は非常に大事です。
説明のために上よりももう少し簡単な関数を用意します。型のみです。

readInteger :: String -> Int

これは例えば"0"(文字列)を0(整数)に変換する関数です。両者は同じゼロですが区別されます。
もうひとつ簡単な関数を挙げておきます。

toUpperString :: String -> String

はい、これは例えば"this"という文字列を"THIS"という文字列に変換する関数です。
さて、getMecabedの型をもう一度見てみましょう。

getMecabed :: String -> IO [String]

見慣れない要素として、IOと[]
の2つが出てきます。これは両方ともHaskellにとって非常に大事な型になってきますが、
今は後者[]のみ説明しておきます。IOについては今はおいておいてください。
[]はリストを表します。例えば

ns = [1, 3, 2, 4, 1]
ss = ["これは", "テスト", "です"]
cs = ['こ', 'れ', 'は', 'テ', 'ス', 'ト', 'で', 'す', '。']

みたいなのです。Haskellは慣習としてリストを表す変数は末尾にsをつけます。
sをつけなくても問題はないのですが、分かりやすいのでsをつけておきましょう。
英語だと複数形はsをつけますよね、そういう気持ちです。
上は、n(Number), s(String), c(Char)で、その複数形という気持ちです。
それぞれの型を確認しておきましょう。
上の式をREPLにコピペして評価(Eval)してください。
そうした後で、

:t cs
:t ns
:t ss

と、一行ずつ打ち込んでみましょう。

*Main Lib> :t cs
cs :: [Char]
*Main Lib> :t ns
ns :: Num a => [a]
*Main Lib> :t ss
ss :: [[Char]]

という結果が得られているはずです。
:t というのは、その後に続くものの型を表示するコマンドになります。
REPLを使っていく上では非常に大事なコマンドなので憶えておきます。
ちなみに、:t のtはtype(「型」)のtです。
一行ずつ結果の説明していくと、

cs :: [Char]

csはChar(文字)のリストだといっています。’こ’、’れ’等は文字です。
複数の文字をまとめて扱う際にリストを使います。残りのns, ssについても考えてみてください。

ns :: Num a => [a]

これは少し予想と外れているんじゃないでしょうか?

ns :: [Int]

みたいのを想定しませんでしたか?
Haskellでは1, 2, 3, とかはNum, すなわちNumber(数字)という扱いになります。
Num aというのはちょっとむずかしいですが、aにはIntとかFloat、Double, などといった数字が入ってきます。
意味合いとしては、仮にここでaをNum(数字)としておいた場合、nsはa(数字)のリストです、と読みます。
Haskellの用語だと型クラス(Type Class)と、多相(polymorphic)に関連してきます。ただ、ここではおいておきます。
別の意味合いだと、nsの型は[Int]でもあり得るし、[Double], [Float]などでもあり得る(多相)、
今の段階ではそれは決まらない、ということになります。
この辺りは関数を定義する際の省エネとして凄い大事になってきます。おいおい見ていきましょう。
今回は最後にssの型を見ておいてひとまず区切っておきます。

ss :: [[Char]]

となっています。これも予想に反しているのではないでしょうか?

ss :: [String]

みたいのを想定しませんでしたか?ここで大事な教訓が2つあります。
ひとつ目の教訓は、上のふたつを眺めると、

[Char] = String

みたいな対応がありそうです。実際そうなります。
Haskellでは、文字列(String)は文字(Char)のリストになります。
2つ目の教訓は、リストのリスト、みたいのを考えることが出来ます。

css =  [['T', 'h', 'i', 's'], ['i', 's'], ['a'], ['p', 'e', 'n']]
:t css

とやってみてください。

css :: [[Char]]

と返ってくるでしょう。文字のリストのリストということです。
cssというのはCharのリストのリストという気持ちで書いています。
ひとつ目の教訓を適用すると、

[['T', 'h', 'i', 's'], ['i', 's'], ['a'], ['p', 'e', 'n']] =
["This", "is", "a", "pen"]

ということになります。上の意味をゆっくり考えてみてください。実際に、

[['T', 'h', 'i', 's'], ['i', 's'], ['a'], ['p', 'e', 'n']] == ["This", "is", "a", "pen"]

と打ち込むと、Trueと返ってきます。
いい機会なので説明しておくと、Haskellは(==)で両辺が同じかを判定できます。
Trueが正しい、Falseが間違い、ということになります。
説明を忘れていました。TrueとFalseはBoolという型を持ちます。
正しいか間違っているかを表すものです。ブールというのは学者の名前です。
Bool代数みたいのを調べてみてください。
長くなりましたので、今回はここまでとします。

シェアする

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

フォローする