STモナドはなぜ変更可能な参照を外へ持ち出せないのか調べてみた

この投稿は、Haskell Advent Calendar 2019 の4日目の記事です。STモナドの仕組みについて書きました。初中級者向けの内容となっています。

一部正確さに欠ける解説があったので内容を訂正しました。詳細はこの記事下部の訂正内容をご確認ください。(2019/12/06)

はじめに

STモナドは、変更可能な参照と破壊的代入操作を扱いつつも、そういった不純な状態を外へ持ち出せないことが型で保証されているモナドです。

最近この「型で状態を外へ持ち出せない仕組み」について調べていて、なるほどなと思ったので、今回はそのあたりのことを書きたいと思います。

流れとしては、最初にSTモナドの概要や使い方を紹介します。次にSTモナドを支える量化やランクN多相に触れ、それらがどのようにSTモナドの安全性を支えているのかを解説していきたいと思います。

なお、内容に誤りがありましたら優しくご指摘いただけると嬉しいです。

STモナドとは

STモナド (strict state-transformer monad) は、状態を扱うモナドです。ここで言う状態とは、命令型言語のように変更可能な参照を意味しています。この変更可能な参照に対して破壊的代入を繰り返しながら計算結果を求めていくのが、STモナドの特徴です。

ところで、同じ状態を扱うモナドにStateモナドがあります。比較すると、Stateモナドは更新の度に都度新しい状態を作成するのに対して、STモナドは状態の使い回しができるので、処理効率的にはSTモナドの方が有利です。例えば巨大なデータ構造を状態として扱う場合は、この差が開くんじゃないでしょうか。

このように効率的だけど不純な汚れ仕事を扱うSTモナドですが、汚れているのはモナドアクションの中だけです。モナドアクションから最終的に出力される計算結果は、ちゃんと純粋な値として取り出すことができます。

STモナドの面白いところは、この不純な状態を外へ持ち出すことができない、ということが型で保証されている点です。無理に持ち出そうとしても型検査で怒られます。

変更可能な参照が外へ漏れ出ることがなければ、他の場所で意図せず変更されてしまう心配はなくなりますよね。

STモナドの型

それではまず、STモナドの型を確認しておきましょう。

ST s a

右から、型変数 a は値を表し、型変数 s はスレッド(state thread)を表します。

このスレッドを表す s は、STモナドの中だけで生存するPhantom Typeです。Phantom Typeということは、実行時には存在しないけれど型検査時には存在する型、ということになりますが、捉え方としては、「STモナドを駆動するために内部的に専用スレッドが作られる。でもそれは実行時ではなく型検査時に」みたいな捉え方で良いと思います。

余談ですが、Thinking with Types という書籍では、STモナドを、

本質的には、phantom s parameterを持った単なるIdentity Monadだよ

と解説しています。

-- ST Monad
ST s a

-- Identity Monad
Identity a

STモナドの使用例

次に、STモナドを使用した簡単な例を見てみましょう。

import Control.Monad
import Control.Monad.ST
import Data.STRef

sum' :: [Int] -> Int
sum' xs = runST $ do
  ref <- newSTRef 0
  forM_ xs $ \i -> do
    modifySTRef ref (+ i)
  readSTRef ref

main :: IO ()
main = print $ sum' [1..10]
-- 結果: 55

sum' は、Int のリストを受け取り、集計した結果を Int として返す関数です。内部では、STモナドを使用して破壊的代入を繰り返しながら計算結果を求めています。STモナドを使用している範囲が関数 sum' の中に限定されていることに注目してください。

では、関数 sum' をもう少し詳しく見てみましょう。

まず、do構文の中で最初に登場する newSTRef は、初期値を与えると変更可能な参照 STRef を持つSTモナドを作成します。つまり、ref <- newSTRef 0 は、初期値 0 を与えてSTモナドを作成し、そこから STRef を取り出して ref に束縛していることになります。以降この ref は状態を扱う参照型として使用します。

次に forM_ xs $ \i -> do で、引数 xs の各要素を先頭から順番に i へ束縛し、 modifySTRef ref (+ i) で、参照型 ref に対して (+ i) の計算結果を破壊的に代入しています。

modifySTRef による破壊的代入を終えると、 readSTRef ref で、参照型 ref から値を読み取り、最後にdo構文の外で runST によって純粋な値を取り出しています。

このように、STモナドの参照型に対する破壊的代入という汚れ仕事は、モナドアクションの中で完全に閉じ込められていて、最後に計算した結果だけを取り出せることが分かりました。

STモナドで使用されるデータ型と各種関数

ここで、先ほどのSTモナドの使用例で登場したデータ型、それと各種関数のシグネチャを確認しておきましょう。

Data.STRef

Data.STRef は、STモナドで変更可能な参照として扱われ、破壊的代入や値の読み込みの際に必要となるデータ型です。

STRef s a

なお、型変数 sa は、先ほど紹介したST s a の型変数と同じ役割になります。

newSTRef

初期値を与えて、初期状態の参照型 STRef s a を持つSTモナドを作成する関数です。

newSTRef :: a -> ST s (STRef s a)

型変数 a は、与える引数によって型が決まり、参照型 STRef の値として設定されます。また、型変数 s は、この時点で型を決定するものが何も無いので、多相型のままスレッドとして設定されます。

ただ、この結果型 ST s (STRef s a) はちょっと変ですね。ST にも STRef にもスレッドを表す型変数 s がいます。

気持ち悪さは残りますが、これについては後ほど解説します。

modifySTRef

参照型 STRef s a に対して関数 a -> a で値の更新をします。

modifySTRef :: STRef s a -> (a -> a) -> ST s ()

readSTRef

参照型 STRef s a の値と状態を読み取ります。

readSTRef :: STRef s a -> ST s a

runST

関数 readSTRef で読み取った内容から値だけを取り出します。なお、ST s a(forall s. ST s a) となっている理由は後述します。

runST :: (forall s. ST s a) -> a

変更可能な参照を外へ持ち出そうとすると

では、関数 newSTRef で変更可能な参照 ST s (STRef s a) を生成し、関数 runSTST s aa に設定されている参照型 (STRef s a) を持ち出そうとしたらどうなるでしょうか。

runST $ newSTRef 100

これは型検査に失敗し、以下のようなエラーメッセージが出力されます。

<interactive>:18:9: error:
• Couldn't match type ‘a’ with ‘STRef s Integer’
    because type variable ‘s’ would escape its scope
  This (rigid, skolem) type variable is bound by
    a type expected by the context:
      forall s. ST s a
    at <interactive>:18:9-18
  Expected type: ST s a
    Actual type: ST s (STRef s Integer)
• In the second argument of ‘($)’, namely ‘newSTRef 100’
  In the expression: runST $ newSTRef 100
  In an equation for ‘it’: it = runST $ newSTRef 100
• Relevant bindings include it :: a (bound at <interactive>:18:1)

なるほど、変更可能な参照 (STRef s a)モナドの外へ持ち出すことはできませんね。

ちなみに先ほどのエラーメッセージでは、 Couldn't match type ‘a’ with ‘STRef s Integer’ because type variable ‘s’ would escape its scope と、スレッドを表す s をスコープの外に出そうとしているから型が合わないんだよと言っています。スコープって何でしょうか?

エラーメッセージから考察する

先ほどのエラーメッセージから forall s. ST s a、つまり関数 runSTシグネチャが鍵を握っていそうです。

runST :: (forall s. ST s a) -> a

runST :: ST s a -> a じゃなくて runST :: (forall s. ST s a) -> a となっているのは、なぜなのでしょうか。

ここで、forall s . を外したらどうなるか実験してみましょう。以下のコードは、Thinking with Types で紹介されていたコードを実験用に改変してみました。(コード全体は こちら になります)

runST' :: ST s a -> a  -- `forall s .` を削除
runST' = unsafeRunST

foo :: STRef s Integer
foo = runST' $ newSTRef 100

こうすると型検査で怒られることなく変更可能な参照 STRef s Integer を外へ持ち出せてしまいました!

やはり、関数 runSTシグネチャSTモナドの安全性を支えるために重要なことが分かりました。

では、この forall って何でしょうか。

forallの探求

量化子は型に量的な意味を持たせる

forall の役割を知るために、まずは以下の関数を見てください。

id :: a -> a
id x = x

関数 id は、型変数 a を受け取りそのまま返す関数です。恒等関数というやつですね。引数は多相型なのでどのような型でも受け取ることができます。

この「どのような型でも」を言語拡張の ExplicitForAll を使用して明示的に書くとこのようになります。

{-# LANGUAGE ExplicitForAll #-}

id :: forall a. a -> a
id x = x

ここで forall が登場しましたね。

この forall は、全称量化子(universal quantifier、記号は)と呼ばれるもので、修飾した型変数に対して文字通り「すべての(for all)」という量的な意味を持たせることができます。つまり、forall a. a は、型変数 a に「すべての型に対応する」という意味を持たせています。*1

このように、全称量化子で型変数を量化する(量的な意味を持たせる)ことを、全称量化(Universal Quantification)と呼びます。*2

そして、通常は特に明示しなくても暗黙的に forall が付与されるので、以下はそれぞれ同じ意味になります。

{-# LANGUAGE ExplicitForAll #-}

{--
それぞれ上から順番に
- forallを省略
- forallを明示
- forallが修飾する範囲を分かりやすくするため括弧を記述
--}

id :: a -> a
id :: forall a. a -> a
id :: forall a. (a -> a)

reverse :: [a] -> [a]
reverse :: forall a. [a] -> [a]
reverse :: forall a. ([a] -> [a])

map :: (a -> b) -> [a] -> [b]
map :: forall a b. (a -> b) -> [a] -> [b]
map :: forall a b. ((a -> b) -> [a] -> [b])

上記のコードで分かることは、暗黙的に付与される forall は、関数の最も外側に置かれ、関数全体を修飾するということです。括弧の中など、関数の内側には置かれません。

さて、全称量化について説明しましたが、結局のところforall を省略してもしなくても同じ意味になるので、forall の役割がまだ漠然としています。

それでは、forall の役割を更に探るため、次はランクN多相へと進みましょう。

量化子を関数の内側で定義する

言語拡張の RankNTypes(ランクN多相)を使用すると、全称量化子 forall を関数の内側で明示的に定義することができます。これにより、修飾した型変数のスコープを限定的にするとともに、型推論の制限を緩和させることができます。ランクN多相の詳細は後述するとして、これが何を意味するのか順を追って説明します。

以下の関数 applyTrue は、引数で受け取った関数 a -> aTrue を設定し、その結果を Bool として返す関数です。

{-# LANGUAGE ExplicitForAll #-}

applyTrue :: forall a. (a -> a) -> Bool
applyTrue f = f True

これは型検査で失敗してしまいます。

• Couldn't match expected type ‘a’ with actual type ‘Bool’
  ‘a’ is a rigid type variable bound by
    the type signature for:
      applyTrue :: forall a. (a -> a) -> Bool
    at...

なぜなら、applyTrue と関数 a -> a との間で、下記の通り期待する型と実際の型が一致していないからです。(より詳しい解説は後ほど)

  • 関数 a -> a は引数に a を期待してるのに Bool を渡している
  • applyTrue の結果型は Bool を期待しているのに関数 a -> aa を返している

このような場合は、言語拡張の RankNTypes を使用して、全称量化子 forall を関数の内側で明示することで、型推論の制限を緩和させます。

{-# LANGUAGE RankNTypes #-}

-- forallを括弧の中に入れる
applyTrue :: (forall a. a -> a) -> Bool
applyTrue f = f True

これは型検査に成功します。

forall a. (a -> a)(forall a. a -> a) としただけですが、applyTrue と関数 forall a. a -> a との間で、期待する型と実際の型が一致したようです。

なぜこのようなことが実現できるのか、ランクN多相についてもっと調べてみる必要がありますね。

ランクN多相の概要と特徴

ランクN多相の概要

ランクN多相(Higher-rank polymorphism)*3 の「ランク」とは、関数の多相性の深さ(forall が修飾する関数の矢印 -> の深さ)を表しており、どのくらい深いかでランクのNが決まります。

具体例を見てみましょう。以下は、下に行くほど上位のランクになります。(ランクが高くなると関数の多相性は深くなります)

t                               -- ランク0単相
forall a. a -> t                -- ランク1多相
(forall a. a -> t) -> t         -- ランク2多相
((forall a. a -> t) -> t) -> t  -- ランク3多相

これでいくと、先ほどの applyTrue はこうなりますね。*4

  • applyTrue :: forall a. (a -> a) -> Bool はランク1多相
  • applyTrue :: (forall a. a -> a) -> Bool はランク2多相

では、ランクが高くなるとどのようなことが起こるのでしょうか。以下は、ランクN多相がもたらす2つの特徴をまとめてみました。

  1. 上位のランクの型は、下位のランクから決定することができないので、型の決定を先送りにできる(型推論の制限を緩和)
  2. 量化された型は、修飾したスコープから外に出られない(スコープ内での型の束縛)

特徴その1: 型推論の制限を緩和

まず1つ目の特徴について。

上位のランクの型は、下位のランクから決定することができないので、型の決定を先送りにできる(型推論の制限を緩和)

先ほどの関数 applyTrue で説明します。

最初に型検査で失敗したときは、applyTrue はランク1多相でした。

{-# LANGUAGE ExplicitForAll #-}

applyTrue :: forall a. (a -> a) -> Bool
applyTrue f = f True

実は forall が関数の最も外側にある場合、型の推論は関数の呼び出し元の責務になります。applyTrue の場合は、呼び出し元が型を推論してしまうので、関数 a -> a はランク0単相として扱われ、不十分な多相性のまま Bool の値を適用したのでエラーになったのでした。

型検査に失敗したときのエラーメッセージは、aBool かどうか分からない型に決定されているので、型が一致しないよと言っているわけですね。

• Couldn't match expected type ‘a’ with actual type ‘Bool’
  ‘a’ is a rigid type variable bound by
    the type signature for:
      applyTrue :: forall a. (a -> a) -> Bool
    at...

その後、applyTrue をランク2多相へ上げることで「applyTrue は呼び出し元から見て上位のランクである」という関係にしたのがこれですね。

{-# LANGUAGE RankNTypes #-}

applyTrue :: (forall a. a -> a) -> Bool
applyTrue f = f True

前述の通り、上位のランクの型は下位のランクから決定することができないので、関数の呼び出し元は型の推論を先送りにします。つまり、型推論の責務は関数の呼び出し先に変わったのです。

では、先送りにされた型の推論はどの時点で行われ型が決定するのか。実は、高ランクな多相型の場合、型を明示しない限り決定できません。明示しなければ型検査で失敗します。

applyTrue の場合は、関数内部で関数 forall a. a -> aBool の値を渡したので、この時点でようやく aBool であると推論されました。その結果、applyTrue と関数 forall a. a -> a との間で、期待する型と実際の型が一致したのです。

型の決定を先送りにするとこんなこともできるよ、という例をもう一つ。

以下の関数 applyTuple は、それぞれ異なる型の要素を持つタプル (True, 'a') に引数で受け取った関数 forall a. a -> a を適用する関数です。

{-# LANGUAGE RankNTypes #-}

applyTuple :: (forall a. a -> a) -> (Bool, Char)
applyTuple f = (f True, f 'a')

applyTrue のときと同じように、applyTuple が呼び出された時点では関数 forall a. a -> a の型の推論は先送りにされています。面白いのが、先送りにされた型の推論は、タプルのそれぞれ異なる型の要素に関数を適用する際、それぞれの型として推論できるのです。

特徴その2: スコープ内での型の束縛

次に、2つ目の特徴についてです。

量化された型は、修飾したスコープから外に出られない(スコープ内での型の束縛)

例えばランク2多相の (forall a. a -> a) は、括弧の中という限定されたスコープに対して型変数 a を量化しています。このような高ランクの型は、修飾したスコープの中で束縛されるので、外へ持ち出すことができません。

つまり、こういうことはできません。

(forall a. a -> a) -> a

そもそもスコープの中と外とではランクが違うので、a はスコープの外では存在できないのです。そして、このようなスコープに束縛された型は、スコーレム定数(skolem constants)と呼ばれています。

forallの探求はここで終わり

ここまでのforallの探求で、STモナドのトリックを解き明かすための準備が整いました。*5

次はいよいよ、STモナドはなぜ変更可能な参照を外へ持ち出せないのかを解き明かしていきます。

STモナドのトリック

それでは、先ほどのSTモナドから変更可能な参照を外へ持ち出そうとしたコードをもう一度見てみましょう。ここから読み解いていきます。

runST $ newSTRef 100

newSTRefの結果型の意味

まずは、newSTRef の部分です。シグネチャはこうでしたね。

newSTRef :: a -> ST s (STRef s a)

ST s aa は値を表しているので、newSTRef の結果型 ST s (STRef s a) で値を表しているのは、参照型の (STRef s a) ということになります。

それとスレッドを表す s ですが、少し前にこの sSTSTRef の両方にいるのが変だと書きました。これは、STs を参照型 STRef にタグ付けすることで、STRefSTs に依存するという関係を作り出していたのです。

型変数 s のもう一つ注目すべき点は、型を推論する情報が何も無いので多相型を維持しているということです。先ほどの newSTRef 100 は、

GHCi> x = 100 :: Int
GHCi> :t newSTRef x
newSTRef x :: ST s (STRef s Int)

s は多相型を維持したままになっています。

runSTの引数の型の意味

次に runSTシグネチャを見てみましょう。

runST :: (forall s. ST s a) -> a

runST は、(forall s. ST s a) -> a とランク2多相で、ST s as だけが限定されたスコープ内で量化されています。そのため、値の a はスコープの外へ持ち出すことができますが、s はスコープ内で束縛されたスコーレム定数なので外へ持ち出すことができません。

ここで疑問です。シグネチャを読み解く限り ST s aa はスコープの外へ持ち出せるはずなのに、なぜこれは失敗するのでしょうか?

runST $ newSTRef 100

実はここで、newSTRef の結果型 ST s (STRef s a) のコレが効いてくるのです。

  • STRefSTs に依存している
  • s は多相型を維持したままになっている

つまり STRef は、スコーレム定数で型を推論するための注釈が何もない s に依存しているので、s に束縛される形でスコープの外へ持ち出すことができないのです。

以上が、STモナドはなぜ変更可能な参照を外へ持ち出せないのかの理由になります。

おわりに

最後まで読んでいただいてありがとうございました!

今回のSTモナドは、Haskell入門 関数型プログラミング言語の基礎と実践 の脚注で「STモナドは、GHCのRankNTypesという拡張を使っていて…」と書かれていて、「え?それってどういうこと?」というところから色々な記事や書籍を読んで、自分なりに理解できたことをまとめてみました。

Haskellの型システムの世界に魅了され今年5月に本格的に入門しましたが、探求すればするほど濃厚な世界が広がって行って楽しいですね。今後も探求を続けたいと思います!

訂正内容(2019/12/06)

以下の点で正確さに欠けていたので記事の一部を訂正しました。

「量化子を関数の内側で定義する」にて、タプルのそれぞれの要素に多相型関数を適用する例を挙げました。

{-# LANGUAGE ExplicitForAll #-}

applyTuple :: forall a. (a -> a) -> (Bool, Char)
applyTuple f = (f True, f 'a')

これに対して、「どちらか一方の型として推論されてしまい、両方の型に対応できない」と解説しました。実はそうではなくて、関数 a -> a の型は applyTuple の呼び出し元が推論してしまうので、関数 a -> a はランク0単相として扱われ、不十分な多相性のままタプルの各要素に適用したからエラーになったのでした。

これは単一の型でもエラーになります。

{-# LANGUAGE ExplicitForAll #-}

applyTrue :: forall a. (a -> a) -> Bool
applyTrue f = f True

この型の推論の制限を緩和し、呼び出された側で型の推論をさせるのが、ランクN多相というわけです。

上記の点、それと脚注のRank2Typesについても加筆修正しています。

@Mizunashi_Manaさん、ご指摘ありがとうございました!
https://twitter.com/Mizunashi_Mana/status/1201886227797950464

参考資料

*1:参考: 全称記号(Wikipedia)

*2:参考: 量化(Wikipedia)

*3:「任意ランク多相(Arbitrary-rank polymorphism)」と表記されることもあります。

*4:一般的にランク2以上はランクNとして表すことが多いようなので言語拡張は RankNTypes を使用しましたが、ランク2用の Rank2Types というのもあります。 Rank2TypesはRankNTypesのエイリアスで、歴史的経緯と互換性のために残されている非推奨の言語拡張なので、こちらの説明は無視してください。(参考

*5:量化やスコーレムは述語論理の概念ですが、残念ながら私は述語論理を分かってないのでこれ以上詳しい説明はできないのです。

VSCodeでHaskellを書く環境を構築する(IntelliJ IDEAの操作感ちょい足し)

はじめに

先日、自宅のMacBook PromacOSクリーンインストールしつつCatalinaにアップデートしました。

まっさらな環境に一から環境構築していくのは楽しい作業ではあるのですが、Haskellの環境に関しては数ヶ月前に試行錯誤の末に構築したこともあり全然整理できていなくて、この機会に手順を書いておこうかなと思います。

環境

前提

macOSに以下のソフトウェアがインストール済みであること。

作成する環境について

これから作成する環境は、VSCodeIDEのような操作感を手に入れたうえで、IntelliJ IDEAの操作感をちょい足ししたような環境を目指します。

そのためには以下のツールを使用します。

Haskell IDE Engine

Haskell IDE Engine は、Language Server Protocol を介して、普通のテキストエディタIDEのようなコード補完やコンパイルエラーの表示などの機能を追加できるツールです。操作イメージは、公式の この辺 を見ると分かりやすいと思います。

github.com

なお、Haskell IDE Engineは、今のところバイナリ形式での配布はされていないため、GitHubからソースコードを落として手元の環境でビルドする必要があります。ビルド時間はそれなりに掛かります。

IntelliJ IDEA Key Bindings

IntelliJ IDEA Key Bindings は、VSCodeのキーバインディングIntelliJ IDEAのそれに置き換えるVSCodeのextensionsです。

これを使用すると、例えばVSCode上のターミナルの開閉は Option+F12 で、プロジェクトウィンドウの開閉は Command+1 でという風に、IntelliJ IDEAのショートカットキーがVSCodeでも使えるようになります。詳しい使い方は、 こちら に記載されています。

そもそもなぜこの環境にしたのか

私は普段の開発にIntelliJ IDEAやAndroid Studioなど、JetBrains製品を愛用しています。そのためHaskellIntelliJ IDEAで書きたかったのですが、専用のpluginである IntelliJ Haskell がどうも自分には合わなくて、それとHaskell IDE Engineとの出会いもあったため普段補助的に使用しているVSCodeHaskellを書くことにしました。

ただ、個人的にVSCodeの素のキーバインディングはいまいち馴染めなくて、JetBrains製品との行き来にどうしても脳内コンテキストスイッチが発生してしまいます。これを防ぐために、IntelliJ IDEA Key Bindingsを使用することにしました。

前置きが長くなりましたが、次項からは手順を書いていきたいと思います。

構築手順

1. Stackをインストール

まずは、Homebrewを使用して Stack をインストールします。 Stackは、Haskellのビルド&パッケージ管理ツールで、プロジェクト毎のGHCや依存パッケージのバージョン管理などができます。

$ brew install haskell-stack

$ stack —version
Version 2.1.3 x86_64

2. PATH の追加

Stackでインストールする各種実行ファイルは、 ~/.local/bin に保存されます。 そのため、 ~/.local/binPATH に追加しておきます。

$ echo 'export PATH=~/.local/bin:$PATH' >> ~/.bashrc
  • fishの場合
$ echo 'set -x PATH ~/.local/bin $PATH' >> ~/.config/fish/config.fish

3. stackコマンドの自動補完を設定

ログインシェルを bashzshにしている場合は、 Shell Auto-completion を設定しておくことで、Stackのサブコマンドを自動補完してくれるようになります。以下はbashの例。

$ echo 'eval "$(stack --bash-completion-script stack)"' >> ~/.bashrc

Shell Auto-completionは、残念ながら fish では未対応のようですね。

4. stack update

以下のコマンドを叩き、 ~/.stack 内のグローバルプロジェクトのフォルダを生成しておきます。

$ stack update

$ tree ~/.stack -L 2
/Users/xxx/.stack
├── config.yaml
├── global-project
│   ├── README.txt
│   ├── stack.yaml
│   └── stack.yaml.lock
├── pantry
│   ├── hackage
│   ├── pantry.sqlite3
│   └── pantry.sqlite3.pantry-write-lock
├── programs
│   └── x86_64-osx
├── setup-exe-cache
│   └── x86_64-osx
├── setup-exe-src
│   ├── setup-mPHDZzAJ.hi
│   ├── setup-mPHDZzAJ.hs
│   ├── setup-mPHDZzAJ.o
│   ├── setup-shim-mPHDZzAJ.hi
│   ├── setup-shim-mPHDZzAJ.hs
│   └── setup-shim-mPHDZzAJ.o
├── snapshots
│   └── x86_64-osx
├── stack.sqlite3
└── stack.sqlite3.pantry-write-lock

5. Haskell IDE Engineをインストール

Haskell IDE Engineをインストールする前に GMP(多倍長演算ライブラリ) を再インストールしておきます。これをやっておかないと、Haskell IDE Engineのビルドの途中でエラーになってしまいます。

$ brew reinstall gmp

Haskell IDE Engineは前述の通りバイナリ形式では配布されていないので、 GitHub からソースコードを落として手元の環境でビルドする必要があります。

まずはgit cloneして、haskell-ide-engineへchange directoryします。

$ git clone https://github.com/haskell/haskell-ide-engine --recurse-submodules
$ cd haskell-ide-engine

取りあえずhelpを確認してみましょう。

$ stack ./install.hs help

run from: stack

Usage:
    stack install.hs <target>
    or
    cabal new-run install.hs --project-file install/shake.project <target>

Targets:
    help                    Show help message including all targets

    build                   Builds hie with all installed GHCs
    build-all               Builds hie for all installed GHC versions and the data files
    build-data              Get the required data-files for `hie` (Hoogle DB)
    hie-8.2.2               Builds hie for GHC version 8.2.2
    hie-8.4.2               Builds hie for GHC version 8.4.2
    hie-8.4.3               Builds hie for GHC version 8.4.3
    hie-8.4.4               Builds hie for GHC version 8.4.4
    hie-8.6.1               Builds hie for GHC version 8.6.1
    hie-8.6.2               Builds hie for GHC version 8.6.2
    hie-8.6.3               Builds hie for GHC version 8.6.3
    hie-8.6.4               Builds hie for GHC version 8.6.4
    hie-8.6.5               Builds hie for GHC version 8.6.5
・・・省略・・・

helpの内容を見ると、GHCのそれぞれのバージョンに対応したHaskell IDE Engineが用意されていることが確認できます。今回は執筆時点の最新である 8.6.5 を指定します。

$ stack ./install.hs hie-8.6.5
$ stack ./install.hs build-data

ちなみに、ここで build-all を指定してしまうと、すべてのGHCのバージョンに対してHaskell IDE Engineをインストールしてしまうので、恐ろしく時間が掛かってしまいます。使用するGHCのバージョンが決まっているのであれば、バージョン指定するのが無難です。

しばらく待っていると、こういう表示が出れば完了です。私の環境で約42分掛かりました。

Copied executables to /Users/xxx/.local/bin:
- hie
- hie-wrapper
# stack (for stack-hie-8.6.5)
Build completed in 42m15s

$ hie --version
Version 0.12.0.0, Git revision 3ec201f76d17148f67efb02672382109342ce391 (3037 commits) x86_64 ghc-8.6.5

6. VSCodeにextensionsを追加

VSCodeのextensionsに以下を追加します。

7. hoogleのセットアップ

最後にhoogleとの同期をできるように以下のコマンドを叩きます。

$ stack install hoogle --resolver lts
$ hoogle generate

8. 完了

手順は以上です。ここまでの手順を終えると、VSCodeこういう操作 ができるようになっているはずです!

おわりに

Haskellを本格的に学ぶようになって半年くらい経ちますが、HaskellIDEやツールがまだまだ成熟していないのかなと感じています。そのため今はこの環境が自分にとって良い選択でも、状況はどんどん変わっていくと思うので、より良い環境は継続的に探していきたいと思います。

参考文献

本気でHaskellに入門したかったので「Get Programming with Haskell」を読んだ

久々の投稿です。

今回は、最近読んだこちらの本がHaskellの入門書としてとても良かったので紹介したいと思います。

www.manning.com

こちらの本は、ManningAmazon で購入することもできますが、サブスクリプション型の O'Reilly Learning (Safari Books Online) でも読むことができます。(ちなみに私はSafariで読みました)

はじめに

私のこれまでのHaskellとの関わりは、数年前に「すごい Haskellたのしく学ぼう! 」(通称: すごいH本)を読んだことがある程度で、その時はHaskellを学ぶことよりも、Monadについての知識を深めることが目的でした。

しかし月日が経過し、心境の変化というか、最近何だか無性に体がHaskellを求め始めてきたので、これを機にちゃんと入門することにしました。

入門するにあたり、すごいH本をもう一度読んでも良かったのですが、どうせなら新しめな本でかつ実践的な内容が書かれている入門書を探していたところ、この本にたどり着いたのです。

この本を読んで得られること

この本は、Haskellの基本が体系的にまとまっていて、しかも文章の構成がとてもうまいので、順番に読み進めていくことでHaskellの以下の知識が身に付くようになっています。

  • 関数型プログラミングの基礎
  • Haskellの基本構文や型、再帰処理
  • 再帰的なデータ構造
  • 代数的データ型(algebraic data type)と各種型クラス(type class)の性質と実装方法
  • IOで不純(impure)な世界の扱い方
  • Haskellの文字列型(String、Text、ByteString)の扱い方
  • GHC拡張
  • Stack(ビルドツール)の利用方法
  • QuickCheckを使用したProperty-based testing
  • JSONエンコード・デコード、DBアクセス、HTTPリクエスト などなど

それと最後まで読んで感じたのが、コードの書き方で、いくつか方法がある場合でも学ぶ人が混乱しないように何種類かに限定し、それを本全体を通して一貫性を持たせていることです。例えば、Haskellにはパターンマッチのやり方が色々あると思いますが、敢えて網羅的ではなく何種類かに絞ったやり方で解説しています。

それ以外にも、let ... in よりも where を、 newtype は紹介こそされていますが、本全体を通してサンプルコードには登場していなかったと思います。また、Monadは、Maybe、List、Either、IOは書かれていますが、Reader、Writer、State等は書かれていないことや、Monad Transformer、Lens、それから並行・並列処理についても書かれていません。

この辺は賛否両論あると思いますが、個人的には、何かの言語を学ぶのに1冊しか読まないということはまず無いと思うので、入門書としてこれはこれで良いのかなと思っています。網羅的に学ぶのであれば、他の本も併せて読んだ方が良さそうです。

見せ方がうまいなと思ったところ

この本は大きく7つのUnitで分かれており、最初の Unit 1 は関数型プログラミングの歴史や利点、Haskellの基本を解説しています。ただし、この時点では型のことは一切触れていないので、まるで動的型付け言語であるかのような見せ方をしています。しかしそれが Unit 2 へ進むことで、実は今まで見てきたものはHaskellの強力な型推論があるからこそ実現できていたことなんだと分かり、そこからHaskellの型の世界へといざなわれていきます。

もう1点、本全体を通して登場する型クラスは、ShowReadNumなど基本的なものから、SemigroupMonoidFunctorApplicativeAlternativeMonad も登場しますが、これらの段階的な登場のさせ方と解説が秀逸なのです。「なるほど、だからこの型クラスがあるのか」と感じさせる。また、もともとHaskellの型クラスは、専用の構文があるので読みやすいのもありますが、それを更に図解していたり、Haskellの歴史も交えて解説している点が非常に良いです。

そう、本全体を通して感じたのが、累進性の実感の原理なのです。

単純なものから始めて、より興味深い視点や入り組んだ視点へと導く方法があります。 景観設計者(landscape architect)は連なった風景を設計するために累進性の実感(progressive realization)の原理を使います。景観設計者は、ものを意図的に隠して景観全体を渡り歩くまでは見えないようにするための、さまざまな眺め(view)を設計します。この考え方は、少しずつ、興味深い段階を踏んで、目指している目的地へと見物人を動かすというものです。あらゆる曲がり角に、新しく、興味深いものがあります。

John Simondsは著書『Landscape Architecture(邦題:ランドスケープ・アーキテクチュア)』の中で次のように述べています。「ある眺めは、計画上最も望ましいとした地点からだけ、最も印象深くその全貌を明らかにすべきである」。それぞれの眺めは、独自に魅力を発揮します。そして、新たな眺めは、それぞれ新たな驚きを含んでいます。累進性の実感により、曲がり角の向こうにあるものへの期待で楽しみが増えるのです。

出典元:「オブジェクトデザイン: ロール、責務、コラボレーションによる設計技法」レベッカ・ワーフスブラック / アラン・マクキーン 著

注意した方が良いところ

1点注意した方が良いところがあって、本の中で、依存ライブラリやデフォルトのGHC拡張の設定を .cabal ファイルに直接編集するように書かれていますが、Stackを使っている場合はpackage.yaml に以下のように記述すると、Stackにバンドルされているhpackが自動的に .cabal ファイルを生成するので、その方法で定義した方が楽だと思います。

dependencies:
- base >= 4.7 && < 5
- aeson
- bytestring
- text

default-extensions:
- OverloadedStrings
- DeriveGeneric

こちらの記事が詳しく解説されていますね。

qiita.com

おわりに

私は、Get Programming with Haskell によってHaskellへの扉が開かれました。次は何を学ぶかは、この本の最後にもヒントが書かれていますし、他にも読みたい本がたくさんあるので、また読んで面白かったらここに書きますね。

追記(2019/07/20)

この記事を公開してから知ったのですが、Get Programming with Haskell の翻訳版が今度出版されるそうです。「英語はちょっと…」という方はこちらをチェックしてみてはいかがでしょうか。

入門Haskellプログラミング

入門Haskellプログラミング

3年間悩み続けてようやく利用開始したSafari Books Onlineが最高だった件

先日、Safari Books Onlineの利用を開始したのですが、学びの世界が一気に広がりとても満足しています。今回はこの喜びを表現しようと、Safari Books Onlineについてとか、利用開始に至った経緯、それと、お得な利用方法について書きたいと思います!

Safari Books Onlineとは

Safari Books Onlineは、海外の200社以上の大手出版社から発行されている書籍、動画などをサブスクリプション型で提供しているデジタルライブラリーサービスです。残念ながら、今のところ日本語のコンテンツは無いようです。

www.oreilly.co.jp

ちなみに、今は「Online Learning with O’Reilly」という名称になったのでしょうか。「Safari Books Online」という名称が公式サイトから見当たらなくなりました。ですが、いまいちしっくりこないので、この記事では昔からの呼び名である「Safari Books Online」で書いていきたいと思います。

www.oreilly.com

利用開始までの長い道のり

実は、このサービス自体はだいぶ昔から知っていましたし、自分の読書スタイルからSafari Books Onlineを利用した方が絶対にお得というのは分かっていたものの、なかなか契約に踏み切れなかったんですね。というのは、小遣い制な私にとって月額$39または年額$399というのは微妙にハードルが高くて、これまで何度も検討しては断念というのを繰り返していたのです。そうこうしている内に、3年の月日が経ってしまいました。

ちなみに私の読書スタイルはこんな感じです。

  • その時の気分によって色んな本を並行読書している
  • 洋書は比較的よく読む
  • 大量の書籍に囲まれて生活していたい

すみません。最後のは願望ですね笑

あと、最近では読書以外に動画でも学ぶことが多くなりました。

利用開始に踏み切れたものは

そんな私がSafari Books Onlineの利用開始に踏み切れたのは、Optimizing Java という本がありまして、これをどうしても読みたかったからなんですね。

shop.oreilly.com

この本、Amazonだと5千円もする!どうせ5千円払うのならSafari Books Onlineを契約し、ついでに他の本も併せて読んだ方がお得なのでは。だけど小遣いが...

そんな時、以下の記事で ACM(Association for Computing Machinery) という、チューリング賞などで有名な米国の計算機学会の会員(年額$99)になると、特典としてSafari Books Onlineが利用できることを知りました。

410gone.click agnozingdays.hatenablog.com

記事を書いてくださった方々、めっちゃ感謝しています!即行で契約しました!

ACM会員の登録方法や、ACMアカウントでSafari Books Onlineを利用する方法については、上記のサイトで紹介されています。もちろん、Webブラウザだけでなく、モバイルアプリからも利用できます!

学びの世界はどう広がったか

圧倒的なコンテンツの量なので、大量の書籍に囲まれて生活していたい私の願望は満たされましたし、その中からその時の気分によって読みたい本をピックアップする感じで並行読書を楽しんでいます。もちろん、マルチデバイスで利用できるので、通勤中はタブレットのモバイルアプリで、自宅や職場でMacを触っているときはWebブラウザで読んでいます。読みたい本を探すときも、大量の書籍の中から串刺し検索で探せるのもいいですね。この機能で、今まで自分の知らなかった書籍にも巡り会えています。

f:id:shinharad:20190222192123p:plain

また、動画のトレーニングコースも充実しているので、最近だとiOSAndroidなど、モバイルアプリ開発の学習に利用しています。やはり、「ちょっと気になる技術がある」に対して最初の入口として動画で学び、深掘りしたくなったら書籍で学ぶというのはとても効率的です。

終わりに

今回は、Safari Books Online最高だったよ!という記事でした。

よく、企業のITエンジニア向けの福利厚生として、書籍購入補助制度があると思います。非常にありがたい制度ではあるのですが、大体の場合は企業の共有財産として購入するのでリアル本に限られたり、読み終わったら共有の本棚に戻す必要あったりすることが多いのではないでしょうか。大量の書籍に囲まれて生活していたい、いつでも読みたい本を自由に選びたい私にとっては、ちょっと物足りないというか...

そんな時、Safari Books Onlineにはチームプランや企業用プランもあるようです*1。ITエンジニア向けの福利厚生としてSafari Books Onlineが使えたら、私のような人がとても喜びます。一部の企業では既に導入済みのところもあるようなので、これがもっと広がるといいなと個人的に思っています。

macOS Mojaveにアップグレードして対処したこと

昨年11月にMacBook ProのOSをMojaveにアップグレードしましたが、今回から導入されたダークモードは個人的にかなり好みではあるものの、普段使いするのにとても困った事象に遭遇しました。今回は発生した事象とどう回避したかについて書きたいと思います。

スリープ中のバッテリー消費が激しくなった

これは、Mojaveアップグレード後の問題としてよく聞く事象だと思います。私の場合、自宅の MacBook Pro (Retina, 13-inch, Early 2015) をMojaveにアップグレードしたところ、この事象が発生するようになりました。

これが発生すると、例えば自宅でMacBook Proをフル充電してスリープモードのまま外出し、いざ使おうとしたら数%しか残っていない!みたいなことが何回かあって非常に困りました。

一部の記事では、macOSハイバネーション機能に問題があって、SMCをリセットすれば解消するみたいなことが書かれていましたが、私の場合それでは解消しませんでした。仕方がないので、電源アダプタを常時持ち歩くとか、使わないときは電源を落とすとかで、しばらく我慢を強いられる状況が続いていたんですね。

そして先日、別の理由でMacBook Proの環境をまっさらにしようと思い、OSのクリーンインストールをしたところ、この事象は嘘のように発生しなくなりました。

原因は結局分かりませんでしたが、今にして思うと一部のMojave未対応のアプリケーションが暴れてたのかなと推測しています。

Chromeでリンクをcommandキー+クリックしても新しいタブで開かなくなった

ページのリンク先を commandキー+クリック で新しいタブとして開いておき、後で順番に見ていくというのはよくやるのですが、お仕事用の MacBook Pro (15-inch, 2017) をMojaveにアップグレードしたところ、この操作をしても新しいタブで開かなくなってしまいました。

これは地味に困る事象で、仕方が無いので後で見るリンク先は毎回コンテキストメニューから「新しいタブを開く」をクリックすることで対処していました。(とても面倒くさい!)

その後、同じ症状で困っている人はいないかなと調べてみたところ、こちらの記事で対処法が紹介されていました。(ありがとうございます。とても助かりました!)

taktakf.hatenablog.com

どうやらUS配列のキーボードで、commandキーに英数/かな切り替えを割り当てるためにKarabiner-Elementsを使用していると発生してしまう事象のようです。

対処としては上記の記事で紹介されているように、commandキーの英数/かな切り替えをKarabiner-Elementsではなく、⌘英かな に変更することで回避することができました。

終わりに

今回発生した事象で、「スリープ中のバッテリー消費が激しくなった」は、自宅のMacBook Proで発生しましたが、お仕事用では発生していません。逆に「Chromeでリンクをcommandキー+クリックしても新しいタブで開かなくなった」は、お仕事用のMacBook Proで発生しましたが、自宅用では発生していません。

モノによって発生する事象が違うみたいですね...困ったことに。

「オブジェクトデザイン」を読んだ

「オブジェクトデザイン」という設計技法の本を読みました。

www.shoeisha.co.jp

オブジェクトデザインは、2007年9月に出版された本で、残念ながら今は絶版となっています。原著は2002年11月に出版されたこちらですね。

www.informit.com

実は先日たまたま図書館の蔵書にあるのを発見し、借りて読むことができたので、今回はこの本について書きたいと思います。

ちなみに、なぜ10年以上前に出版された本を読もうと思ったのかというと、最近までEric Evansの「ドメイン駆動設計」を読み返していたのですが、その中で「責務駆動設計」というキーワードが脳裏に焼き付いていて、ちょうど同じ頃に @j5ik2o さんの「ドメインオブジェクトの責務について」という記事の中でこの本の存在を知ったのがきっかけでした。

この本のテーマ

この本では、オブジェクト指向設計について次のように解説しています。

実世界に存在しないオブジェクトを考え出すことで、現実世界の情報、プロセス、相互作用、関係、そしてエラーでさえも表現します。生命のないものに対して、生命と知性を与えます。理解が困難な現実世界のオブジェクトを、よりシンプルで管理しやすいソフトウェアオブジェクトへと分割します。

オブジェクト指向ソフトウェア開発は、その根本において実世界の物理学に従わないものです。現実世界をオブジェクト機構にモデリングすることが私たちの目的ではありません。そのため、私たちには現実世界を新たに考え出す資格があります。

この、管理しやすいソフトウェアオブジェクトへと分割するために、ロール、責務、コラボレーションという観点から設計を考えていく、というのがこの本の全体的なテーマとなっています。

本の構成

この本は大きく2つの部分で構成されています。

前半の第1章から第6章は、設計概念や責務駆動設計の中心的原理、実践方法について書かれています。前半を読むことで、設計の目的である、一貫した使いやすいオブジェクトを作り上げるために、ロール、責務、コラボレーションがどう作用するのかについて考えるための準備ができます。

後半の第7章から第10章は、前半で得た知識をもとに、ドキュメンテーションやコラボレーションにおける信頼領域内外での戦略、システムの柔軟性について解説しています。

ロール、責務、コラボレーション

アプリケーションは相互作用するオブジェクトの集合であり、それぞれのオブジェクトには ロール(役割) を割り当てます。このロールについて、この本では以下のような単純化した ロールステレオタイプ でオブジェクトを分類することで、責務に注目しやすくしています。

  • 情報保持役(Information Holder): 情報を知り、情報を提供する
  • 構造化役(Structurer): オブジェクト間の関係と、それらの関係についての情報を維持する
  • サービス提供役(Service Provider): 仕事を行うが、一般に演算サービスを提供する
  • 調整役(Coordinator): 他のオブジェクトにタスクを委譲することでイベントに対応する
  • 制御役(Controller): 判断を行い、他のオブジェクトのアクションをしっかりと指示する

責務 は、オブジェクトについて大雑把に記述したもので、本から引用すると以下の3つの主要な項目を含んでいます。

  • オブジェクトが行う動作
  • オブジェクトが持つ知識
  • オブジェクトが他に影響を与える主要な判断

コラボレーション は、オブジェクトもしくはロール(またはこの両方)の相互作用のことですね。

これらを踏まえて本の前半では、初期の探究的設計で作り上げた概念モデルに対して、オブジェクトを見つけ出し、ロールステレオタイプでオブジェクトの役割を単純化したうえで、それぞれの責務やコラボレーションを考えていくという流れで解説しています。あと、ロールや責務に応じたオブジェクトの名前付けのガイドラインについても解説があるので、個人的にとても参考になりました。

累進性の実感

もうひとつ、この本の中で印象に残った箇所について紹介します。

第7章「コラボレーションの記述」では、設計したオブジェクトをどのようにチームに伝えるかという、ドキュメンテーションの話になっています。この中で印象的だったのが、

7.8.2 ストーリーを展開する

単純なものから始めて、より興味深い視点や入り組んだ視点へと導く方法があります。 景観設計者(landscape architect)は連なった風景を設計するために累進性の実感(progressive realization)の原理を使います。景観設計者は、ものを意図的に隠して景観全体を渡り歩くまでは見えないようにするための、さまざまな眺め(view)を設計します。この考え方は、少しずつ、興味深い段階を踏んで、目指している目的地へと見物人を動かすというものです。あらゆる曲がり角に、新しく、興味深いものがあります。

John Simondsは著書『Landscape Architecture(邦題:ランドスケープ・アーキテクチュア)』の中で次のように述べています。「ある眺めは、計画上最も望ましいとした地点からだけ、最も印象深くその全貌を明らかにすべきである」。それぞれの眺めは、独自に魅力を発揮します。そして、新たな眺めは、それぞれ新たな驚きを含んでいます。累進性の実感により、曲がり角の向こうにあるものへの期待で楽しみが増えるのです。

ソフトウェアの設計者も、読み手がコラボレーションの景観を渡り歩くにつれてより深く理解するように仕向けることができます。

これは、ドキュメンテーションに限った話ではなく、例えばプログラミングであれば、宣言的にWHATを明示し、必要になったらHOWを探求させるような構成で書いておけば、累進性の実感を促すことにつながるのかなと思いました。

終わりに

オブジェクトデザインは非常に学びの多い本でした。

今回紹介した内容以外にも、実践するためのガイドラインが多く書かれています。 もちろん、10年以上前に出版された本なので、内容が古い部分もあるのですが、それらは脳内で補完しながら読む必要があります。

個人的には、恐らく1回読んだだけでは理解できていない部分があると思うので、間隔を空けて数カ月後にもう一度読んでみたいと思います。

Monix v3.0.0-RC2からTask.applyの挙動が変わっている

最近、Monix というScalaの非同期プログラミング用ライブラリを使い始めました。

monix.io

最初は、最新安定版である v2.3.3 で色々触ってみて「これは使える!楽しい!」となって、v3.x 系の方はどんな感じかなと思いバージョンを上げてみると、メソッド名をはじめ、色々変わりまくっていることに焦りました。その中で Task.apply の挙動の変更には注意が必要だなと思ったので書いておきます。

v3.x系でのTask.applyの変更

v2.x 系では、以下のコードは非同期処理になっていたと思います。

val task = Task { // Task.apply
  1 + 2
}

ところが、v3.x 系ではこの挙動が同期処理に変わるようです。つまり、Task.applyTask.eval は同じ挙動になる。

Task.applyのコードを比較してみる

それでは、v2.x 系と v3.x 系の Task のコードを比較してみましょう。以下は抜粋です。

Monix v2.3.3

object Task extends TaskInstances {
  /** Returns a new task that, when executed, will emit the result of
    * the given function, executed asynchronously.
    *
    * @param f is the callback to execute asynchronously
    */
  def apply[A](f: => A): Task[A] =
    fork(eval(f))

https://github.com/monix/monix/blob/v2.3.3/monix-eval/shared/src/main/scala/monix/eval/Task.scala#L662-L669

Monix v3.0.0-RC2

object Task extends TaskInstancesLevel1 {
  /** Lifts the given thunk in the `Task` context, processing it synchronously
    * when the task gets evaluated.
    *
    * This is an alias for:
    *
    * {{{
    *   val thunk = () => 42
    *   Task.eval(thunk())
    * }}}
    *
    * WARN: behavior of `Task.apply` has changed since 3.0.0-RC2.
    * Before the change (during Monix 2.x series), this operation was forcing
    * a fork, being equivalent to the new [[Task.evalAsync]].
    *
    * Switch to [[Task.evalAsync]] if you wish the old behavior, or combine
    * [[Task.eval]] with [[Task.executeAsync]].
    */
  def apply[A](@deprecatedName('f) a: => A): Task[A] =
    eval(a)

https://github.com/monix/monix/blob/v3.0.0-RC2/monix-eval/shared/src/main/scala/monix/eval/Task.scala#L2376-L2395

v3.x 系のコメントにこのような記述があります。

WARN: behavior of Task.apply has changed since 3.0.0-RC2. Before the change (during Monix 2.x series), this operation was forcing a fork, being equivalent to the new Task.evalAsync.

以下は機械翻訳

警告:Task.applyの動作が3.0.0-RC2以降に変更されました。変更前(Monix 2.xシリーズ中)、この操作は新しいTask.evalAsyncと同等のフォークを強制していました。

つまり、v3.x 系で非同期な Task を使いたかったら Task.apply ではなく Task.evalAsync を使ってねということみたいです。

終わりに

今回の変更を受けて、 今後は Task.apply ではなく、Task.evalAsync で非同期なのか Task.eval で同期なのかを明示的に書いた方が良いのかなと思いました。v3.x 系では他にも Callback が任意のエラー型を設定できるようになってたりと大きな変更があるみたいですね。