このシリーズでは 2018/5/22 にリリースされた PureScript 0.12.0 で何がどう変わったかを紹介していきます。
今回は ST
の話です1。
なおこの記事は PureScript に触れたことがある読者を想定しています。
PureScript 自体について知りたい場合には他の Qiita の記事や 実例によるPureScript などが参考になると思います。
Effect にならなかった ST
前回の記事では新しく追加された Effect ついて書きました。
この Effect
の追加に伴い、これまで Eff を使用していた多くのライブラリが Effect
を使うよう更新されています。
例えば purescript-console の log 関数は 3.0.0 から 4.0.0 のバージョンアップで下記のように変更されています2。
log :: forall eff. String -> Eff (console :: CONSOLE | eff) Unit
log :: String -> Effect Unit
CONSOLE の他にも RANDOM、REF、EXCEPTION 等の作用がライブラリのバージョンアップにより Effect
に置き換わっています。
例外が ST です。
ST
はこうなりました。
newSTRef :: forall a h r. a -> Eff (st :: ST h | r) (STRef h a)
readSTRef :: forall a h r. STRef h a -> Eff (st :: ST h | r) a
writeSTRef :: forall a h r. STRef h a -> a -> Eff (st :: ST h | r) a
modifySTRef :: forall a h r. STRef h a -> (a -> a) -> Eff (st :: ST h | r) a
runST :: forall a r. (forall h. Eff (st :: ST h | r) a) -> Eff r a
pureST :: forall a. (forall h. Eff (st :: ST h) a) -> a
new :: forall a r. a -> ST r (STRef r a)
read :: forall a r. STRef r a -> ST r a
write :: forall a r. a -> STRef r a -> ST r a
modify :: forall r a. (a -> a) -> STRef r a -> ST r a
run :: forall a. (forall r. ST r a) -> a
関数名や引数の順番も変更されていますが、それはともかく Eff
が Effect
にはならず、かわりに新しく追加された型コンストラクタ ST
を使うようになっています。
それで何が変わったのか?
計算の実行方法が 1つになった
purescript-st 3.0.0 までの ST
では runST
、pureST
関数だけでなく、main関数等で返した作用(Eff)が処理系で実行されることでも計算を行うことが出来ました3。
onePlusOne ∷ ∀ eff. Eff eff Int
onePlusOne = runST do
ref ← newSTRef 1
modifySTRef ref (_ + 1)
onePlusOne ∷ Int
onePlusOne = pureST do
ref ← newSTRef 1
modifySTRef ref (_ + 1)
onePlusOne ∷ ∀ h eff. Eff (st ∷ ST h | eff) Int
onePlusOne = do
ref ← newSTRef 1
modifySTRef ref (_ + 1)
main ∷ ∀ h eff. Eff (console ∷ CONSOLE, st ∷ ST h | eff) Unit
main = do
result ← onePlusOne
logShow result
しかし purescript-st の 4.0.0 では run
関数でしか計算を実行出来なくなりました。
onePlusOne ∷ Int
onePlusOne = run do
ref ← new 1
modify (_ + 1) ref
3.0.0 と 4.0.0 の例を比較すると、いくつかあった計算の実行方法のうち pureST
だけが残って run
関数になったことが判ります。
他の作用と混在出来なくなった
3.0.0 では下の例のように他の作用と混在することが出来ました。
-- 1~10 の間の乱数を 10回加算した結果を返す
sumRandom ∷ ∀ h eff. Eff (random ∷ RANDOM, st ∷ ST h | eff) Int
sumRandom = do
ref ← newSTRef 0
forE 0 10 \_ → do
n ← randomInt 1 10
void $ modifySTRef ref (_ + n)
readSTRef ref
しかし 4.0.0 の ST
は Effect
と決別したため他の作用と混在出来なくなりました。
このようなときは Ref
を使います。
import Effect.Ref (modify, new, read)
sumRandom ∷ Effect Int
sumRandom = do
ref ← new 0
forE 0 10 \_ → do
n ← randomInt 1 10
void $ modify (_ + n) ref
read ref
また、コンパイル後の JavaScript のコードがちょっと大変なことになりますが、StateT
を使うことも出来ます。
sumRandom ∷ Effect Int
sumRandom = 0 # execStateT do
for_ (0 .. 9) \_ → do
n ← lift $ randomInt 1 10
modify (_ + n)
コンパイラによる最適化は行われるのか?
PureScript のコンパイラは 3.0.0 までの ST を下記のように最適化します。
culc ∷ Int → Int
culc n = pureST do
ref ← newSTRef n
_ ← modifySTRef ref (_ + 1)
_ ← modifySTRef ref (_ * 2)
modifySTRef ref (_ + 3)
var culc = function (n) {
return Control_Monad_ST.pureST(function __do() {
var v = Control_Monad_ST.newSTRef(n)();
var v1 = Control_Monad_ST.modifySTRef(v)(function (v1) {
return v1 + 1 | 0;
})();
var v2 = Control_Monad_ST.modifySTRef(v)(function (v2) {
return v2 * 2 | 0;
})();
return Control_Monad_ST.modifySTRef(v)(function (v3) {
return v3 + 3 | 0;
})();
});
};
ぱっと見どこが最適化されているか判りにくいですが、この例では bind
が展開されています4。
しかし 4.0.0 の ST
ではこのような最適化は行われません。
culc ∷ Int → Int
culc n = run do
ref ← new n
_ ← modify (_ + 1) ref
_ ← modify (_ * 2) ref
modify (_ + 3) ref
var culc = function (n) {
return Control_Monad_ST_Internal.run(Control_Bind.bind(Control_Monad_ST_Internal.bindST)(Control_Monad_ST_Internal["new"](n))(function (v) {
return Control_Bind.bind(Control_Monad_ST_Internal.bindST)(Control_Monad_ST_Internal.modify(function (v1) {
return v1 + 1 | 0;
})(v))(function (v1) {
return Control_Bind.bind(Control_Monad_ST_Internal.bindST)(Control_Monad_ST_Internal.modify(function (v2) {
return v2 * 2 | 0;
})(v))(function (v2) {
return Control_Monad_ST_Internal.modify(function (v3) {
return v3 + 3 | 0;
})(v);
});
});
}));
};
これについては下記 Issue がありますのでいずれ最適化されるようになるかもしれません。
なぜ Effect にならなかったのか?
正直 Issue でのやりとりを充分に理解出来ていませんが、今までの ST
ですと Ref
と性質や役割がかなり重なっていましたので、そこを明確に分けるために Effect
にしなかった、と自分は理解しています。
実際 pureST
を使用しているケースを除けば、従来の ST
を使ったコードはほぼそのまま Ref
で書くことが出来ます。
そして Ref
を使えば今までと同様の最適化も行われます。
このあたりは用途に応じて上手に使い分けていきたいところです。
-
厳密に言えば PureScript 本体ではなくライブラリの話になりますが、0.12.0 に伴う変更ということでこのシリーズで扱います。 ↩
-
2018/5/24 にはさらに 4.1.0 に更新され
MonadEffect
を使うようになっていますがここでは触れません。 ↩ -
厳密に言えば
runST
もEff
の row からST
を外すだけであり(runST
の中身は unsafeCoerce と一緒)、計算は処理系での作用の実行時に行われます。 ↩ -
ここでは 4.0.0 との比較のために
pureST
を使いましたが、runST
を使えばbind
だけでなくさらにnewSTRef
やmodifySTRef
も展開されます。 ↩