ブログジェネレータを作りながらHaskellを学ぶ
やっている本はこれ。
一旦4章をすでに始めてしまっていたので4章で学んだことを書いておく。3章も結構あったから、あとで埋めようと思う。
再帰
Haskellにはループのための構文はないので、再帰を使って表現することになる。再帰というと、どうしてもスタックオーバーフローを起こしやすいイメージがあるが、Haskellの場合は遅延評価と相まって効率よくさばける感じになっているらしい。このあたりは、下記の記事を読むとおもしろそう。
- Substitution and Equational Reasoning: https://gilmi.me/blog/post/2020/10/01/substitution-and-equational-reasoning
- The Haskell Heap: http://blog.ezyang.com/2011/04/the-haskell-heap/
再帰には次の3つの要素があり、どれをどう対応させるかを考えながら書くと書きやすくなる。私も再帰は苦手なのでこれは助かる…
- Finding the base case (the most simple cases - the ones we already know how to answer)
- Figuring out how to reduce the problem to something simpler (so it gets closer to the base case)
- Mitigating the difference between the reduced version and the solution we need to provide
部分関数
一応機能としては存在しているが、基本的に使うのも書くのも避けるように、とのこと。理由としては、実行時に無限ループを起こしたり実行時例外を引き起こすことがあるためである。できるだけ場合分けにおける例外を潰して全ケース網羅しやすいようにデータ構造を設計しましょう、みたいな感じなのかな。
Semigroup
要するに結合演算を型をまたいで提供できるようにするもの。Scalaで履修済みだったので、あまり新しい発見だったというわけではないけど、出てきたのでテンションが上がった。
もともとあった append_
関数を次のように書き換え、
instance Semigroup Structure where
(<>) a b = Structure (getStructureString a <> getStructureString b)
さらに、呼び出し部分で次のように書き換えたら完成。
myhtml :: Html
myhtml =
html_
"My title"
( h1_ "heading"
<> p_ "Paragraph #1"
<> code_ "<html></html>"
)
unwords, words
- unwords:
[String]
をString
に結合する。結合時にはスペース区切りを採用する。 - words: スペースで区切られた文字列を分割し、配列にする。
ghci> unwords ["a", "b", "c"]
"a b c"
ghci> words "a b c"
["a", "b", "c"]
unwords . words
すると実質空白トリミングとして機能するっぽい。
maybe id (:)
次のコードがめっちゃ難しく感じた。
maybe id (:) context (parseLines Nothing rest)
maybe id (:)
は、型の遷移を見ると次のようになっている。
maybe id (:) :: Maybe a -> [a] -> [a]
要するに、何かあるかもしれないしないかもしれない値を与えると、それを配列にして返してくれるというものだと思う。(:)
それ自体には前方へのappendの意味があるみたい。(:) x xs
とすると、xs
という配列の先頭にx
を足すみたいな意味になりそう。
maybe
それ自体は、第一引数にデフォルト値、第二引数に関数、第三引数にMaybe
型の値を入れて利用できる。つまり、context
がNothing
だった場合、id
が返る。Just
だった場合、(:)
が適用される、みたいな感じかと思われる。
5章に入った。
qualified imports
要するに関数などの呼び出しの先頭にprefixをつけてくれる機能っぽい。たとえば、import qualified Html
とすると、Html.h1_
などのような呼び出しをしなければならなくなる。関数がどの名前空間にいるものなのかは意外にわかりにくく、個人的には必須の機能なように思われる。なお、as
キーワードを後ろにつけると別名として管理できる。
$
マークダウンからHTMLに変換する実装を書いている最中に出てきた演算子。
convertStructure :: Markup.Structure -> Html.Structure
convertStructure structure =
case structure of
Markup.Heading 1 txt ->
Html.h1_ txt
Markup.Paragraph p ->
Html.p_ p
Markup.UnorderedList list ->
Html.ul_ $ map Html.p_ list
Markup.OrderedList list ->
Html.ol_ $ map Html.p_ list
Markup.CodeBlock list ->
Html.code_ (unlines list)
たとえば Html.ul_ $ map Html.p_ list
について、この演算子を用いない記法で書き直すと、次のようになる。
-- Html.ul_ $ map Html.p_ list
Html.ul_ (map Html.p_ list)
IO
いわゆるOpaque型になっていて、今回実装した Html
型のように、内部の実装を覆い隠すための型。ただ、IO型はbuilt-in型になっていて、実装それ自体もHaskellによって隠されている感じになっているらしい。
自分のイメージとしては、基本的に入出力に関する操作を表す型になっていると思っている。Haskellはいわゆる純粋関数型言語と呼ばれるものに分類されるが、純粋であるためには副作用は締め出されていなければならない。入出力というのは外部システムに対して作用を与えるので副作用を持つことから純粋ではなくなるが、IO
を使って操作させているうちは、そうした非純粋性をopaqueできる、という感じに理解している(合ってるかは自信ない)。
いくつか魔術のような記号が生えていて、それを覚える必要があるのでメモ。
-
>>=
: flatMap -
*>
と>>
: はじめて見たような気もするし、cats-effectにもあったような気がするけど、要するにこの記号でつなげると、最初の計算を実行し、結果を破棄し、次の計算を実行する、という処理を行わせることができる。 -
pure
: IOにリフトできる。 -
<$>
、ないしはfmap
: 要するにmap
?ただし、IOという箱を保ちながら操作させられるmap
という感じ?
do notation
Scalaのfor-yieldとほぼ同じ。>>=
すると書くのが大変になるケースがあるので、それのためのシンタックスシュガー。
FunctorとApplicative
Functorはmap、Applicativeはmapのリフト版。SemigroupとMonoidの関係性と同じ。
optparse-applicativeというライブラリを使うことになる。このライブラリでは、Parser
という型がコンテナとなっていて、この型の中でさらに型を遷移させて処理を進める。Applicativeはこうしたケースにおいて役に立つ。
関数合成がいまいちわからないと思うことが多いのだが、ちょうど細かく解説している記事があった。というメモ。