Skip to content

PureScript 0.12.0 で何が変わったか - ST編

メモ

この記事は 2018/6/8 に Qiita へ投稿したものです。

このシリーズでは 2018/5/22 にリリースされた PureScript 0.12.0 で何がどう変わったかを紹介していきます。

今回は ST の話です1

なおこの記事は PureScript に触れたことがある読者を想定しています。
PureScript 自体について知りたい場合には他の Qiita の記事実例によるPureScript などが参考になると思います。

Effect にならなかった ST

前回の記事では新しく追加された Effect ついて書きました。

この Effect の追加に伴い、これまで Eff を使用していた多くのライブラリが Effect を使うよう更新されています。

例えば purescript-consolelog 関数は 3.0.0 から 4.0.0 のバージョンアップで下記のように変更されています2

1
2
-- purescript-console-3.0.0
log :: forall eff. String -> Eff (console :: CONSOLE | eff) Unit
1
2
-- purescript-console-4.0.0
log :: String -> Effect Unit

CONSOLE の他にも RANDOMREFEXCEPTION 等の作用がライブラリのバージョンアップにより Effect に置き換わっています。

例外が ST です。

ST はこうなりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- purescript-st-3.0.0

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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- purescript-st-4.0.0

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

関数名や引数の順番も変更されていますが、それはともかく EffEffect にはならず、かわりに新しく追加された型コンストラクタ ST を使うようになっています。

それで何が変わったのか?

計算の実行方法が 1つになった

purescript-st 3.0.0 までの ST では runSTpureST 関数だけでなく、main関数等で返した作用(Eff)が処理系で実行されることでも計算を行うことが出来ました3

1
2
3
4
5
6
-- 3.0.0 runSTによる計算例

onePlusOne   eff. Eff eff Int
onePlusOne = runST do
  ref  newSTRef 1
  modifySTRef ref (_ + 1)
1
2
3
4
5
6
-- 3.0.0 pureSTによる計算例

onePlusOne  Int
onePlusOne = pureST do
  ref  newSTRef 1
  modifySTRef ref (_ + 1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 3.0.0 main関数による計算例

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 関数でしか計算を実行出来なくなりました。

1
2
3
4
5
6
-- 4.0.0 での計算例

onePlusOne  Int
onePlusOne = run do
  ref  new 1
  modify (_ + 1) ref

3.0.0 と 4.0.0 の例を比較すると、いくつかあった計算の実行方法のうち pureST だけが残って run 関数になったことが判ります。

他の作用と混在出来なくなった

3.0.0 では下の例のように他の作用と混在することが出来ました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- 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 の STEffect と決別したため他の作用と混在出来なくなりました。

このようなときは Ref を使います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 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 を使うことも出来ます。

1
2
3
4
5
6
7
-- StateT を使った作用との混在例

sumRandom  Effect Int
sumRandom = 0 # execStateT do
  for_ (0 .. 9) \_  do
    n  lift $ randomInt 1 10
    modify (_ + n)

コンパイラによる最適化は行われるのか?

PureScript のコンパイラは 3.0.0 までの ST を下記のように最適化します。

1
2
3
4
5
6
7
8
-- 3.0.0 コンパイル前

culc  Int  Int
culc n = pureST do
  ref  newSTRef n
  _  modifySTRef ref (_ + 1)
  _  modifySTRef ref (_ * 2)
  modifySTRef ref (_ + 3)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 3.0.0 コンパイル後

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 ではこのような最適化は行われません。

1
2
3
4
5
6
7
8
-- 4.0.0 コンパイル前

culc  Int  Int
culc n = run do
  ref  new n
  _  modify (_ + 1) ref
  _  modify (_ * 2) ref
  modify (_ + 3) ref
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 4.0.0コンパイル後

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 を使えば今までと同様の最適化も行われます。

このあたりは用途に応じて上手に使い分けていきたいところです。


  1. 厳密に言えば PureScript 本体ではなくライブラリの話になりますが、0.12.0 に伴う変更ということでこのシリーズで扱います。 

  2. 2018/5/24 にはさらに 4.1.0 に更新され MonadEffect を使うようになっていますがここでは触れません。 

  3. 厳密に言えば runSTEff の row から ST を外すだけであり(runST の中身は unsafeCoerce と一緒)、計算は処理系での作用の実行時に行われます。 

  4. ここでは 4.0.0 との比較のために pureST を使いましたが、runST を使えば bind だけでなくさらに newSTRefmodifySTRef も展開されます。 


Last update: April 5, 2020