なぜHaskellで自然言語処理か
文字列処理力を上げようと思ったのだが、いつも通りC++やらPythonやらを使うのはなんかふつうな気がしたのだ
まあC++で文字列処理ってきほんしぬとはおもうけど
それを考えると普段業務で使っていないHaskellやRustが候補にあがる。
Haskellはかなり長い間使っていなかったので今回は久しぶりにHaskellでやることにした。
ちなみに筆者のHaskell力はLYAHFGGとRWHの気になる部分を読んでみた程度です。
そういう意味ではHaskellやってみたい人向けでもあるかもしれない。
とりあえずは言語処理100本ノックの第1章をやってみる。
言語処理にはあまり明るくないが最初の数章はふつうのテキスト処理問題に思える。
Python用の問題集らしいがまあ、最初の方だけならなんとかなるだろう。
解答に関して
簡潔さとわかりやすさ重視で書きました。
が、Haskell初心者がかいたものなのでたいして簡潔になってないかもしれません。
ただ、わかりやすさと天秤にかけたらわかりやすさのほうがおもいかな。
Haskellは抽象的にかけるぶんむずかしくなってしまうので。
*2018/12/12追記
解答でしょっちゅうString型を使っていますが今はText型のほうが主流のようなのでみなさんはText型を使ってみることをおすすめします。
まあ最初のうちはString型で慣れるのもよいとは思いますが。。
問題と解答
00. 文字列の逆順
文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.
main = putStrLn $ reverse "stressed"
desserts
もはやいうことはあるまい。
ただ単に文字列に対してreverse
を適用しているだけ。
01. 「パタトクカシーー」
「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.
main = putStrLn ["パタトクカシーー" !! index | index <- [0, 2, 4, 6]]
パトカー
**リスト内包表記
**を使用した。map
使うのと大差ないだろうがこの問題に関してはリスト内包表記のほうが簡潔にかけるとおもった。
雰囲気的にはパタトクカシーー !! 0とパタトクカシーー !! 2とパタトクカシーー !! 4とパタトクカシーー !! 6が並んでいるイメージ(こなみ)
02. 「パトカー」+「タクシー」=「パタトクカシーー」
「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.
import Data.List
main = do
let objs = transpose ["パトカー", "タクシー"] -- objs == ["パタ", "トク", "カシ", "ーー"]
putStrLn $ foldr (++) [] objs
パタトクカシーー
なんかこれに関しては一発でなんとかなる関数がありそうだが。。
畳み込みの練習になってしまった。
2018/11/9追記
foldr (++) []
はconcat
関数と同義のようだ。
なので以下のようにも書ける。
import Data.List
main = do
let objs = transpose ["パトカー", "タクシー"] -- objs == ["パタ", "トク", "カシ", "ーー"]
putStrLn $ concat objs
03. 円周率
"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.
module Main where
import qualified Data.List as List
import qualified Data.Char as Char
import qualified System.IO as IO
-- アルファベットのみのリストを生成し、その大きさを返す
numAlphas :: String -> Int
numAlphas = List.length . List.filter Char.isAlpha
main :: IO()
main = IO.print answer
where
ws = List.words "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
answer = List.map Main.numAlphas ws
[3,1,4,1,5,9,2,6,5,3,5,8,9,7,9]
いろいろとコンパイラに怒られ、とうとうimport qualified
を使用することにした。
地味にmap
とfilter
が初登場だ。
main
の書き方はこれでいいのかなかなかわからん。
とりあえずwhere
の中が短いのでここではwhere
でかいた。
最初、問題の意図を勘違いしてかなり時間をかけてしまったが、
numAlphas
関数はその勘違いの副産物なのでよかったとしよう。
正直、こういう問題だったら出力例みたいなのも問題と一緒にあっていい気がするが
04. 元素記号
"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.
module Main where
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import qualified System.IO as IO
makeKey :: (Eq a1, Num a1) => a1 -> [a2] -> [a2]
makeKey n = List.take (takeNum n) -- List.take (1 or 2)
where takeNum n' = if n' `List.elem` [1, 5, 6, 7, 8, 9, 15, 16, 19] then 1 else 2
main :: IO()
main = do
let words = List.words "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
values = [1..(List.length words)] -- values == [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
keys = List.zipWith Main.makeKey values words -- keys == ["H","He","Li","Be","B","C","N","O","F","Ne","Na","Mi","Al","Si","P","S","Cl","Ar","K","Ca"]
map = Map.fromList $ List.zip values keys
IO.print map
fromList [(1,"H"),(2,"He"),(3,"Li"),(4,"Be"),(5,"B"),(6,"C"),(7,"N"),(8,"O"),(9,"F"),(10,"Ne"),(11,"Na"),(12,"Mi"),(13,"Al"),(14,"Si"),(15,"P"),(16,"S"),(17,"Cl"),(18,"Ar"),(19,"K"),(20,"Ca")]
これもどうするかわりと悩んだ。
1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語
は先頭の1文字
を、それ以外の2, 3, 4, 10, 11, ..., 20番目の単語
は2文字
を取り出すので、
[1,2,2,2,1,1,1,..(中略)..,2,1,2]
のようなリストを準備しておけばもっと簡潔になるかと思ったが、
変更に弱そうだし、わかりづらそうだし、やめときました。
makeKey n xs
はn
が[1, 5, 6, 7, 8, 9, 15, 16, 19]
内に存在していたらtake 1 xs
、それ以外ならtake 2 xs
となる関数です。
全体的にはzip
とzipWith
でくっつけていく感じになってます。
第1章前半を終えた感想
はっきり言って結構時間がかかってしまった。
なれない言語だとゴリ押しが効かないのでなかなかキツイ。
とりあえずここまでならhoogleでData.ListとData.Mapの中を覗きまくる方針でよさそう。
1章の後半に行く前に他の言語でどれくらいの時間で最初の5問が解けるかはかってみたいところだなあ。