Help us understand the problem. What is going on with this article?

Haskell副作用ポエム

More than 1 year has passed since last update.

この記事は,「whywaita advent calendar 2017」の22日目の記事です.

真面目な記事を一本書いてしまったので,バランスを取るため(?)不真面目な記事をぶっこんでおこうというのが,この記事の主たる動機です.特に読む価値はありません.メリークリスマス(まだ早いか).

あと@whywaitaは最後の行だけ目を通してください.

概要

Haskellを触り始め,最初に目につくのがIOというヘンテコな2文字だ.多くの近代人は,まずはGoogle検索を訪問するだろう.そして,うんざりして画面を閉じる.そこに書いてあることはこうだ.

Haskellは純粋関数型で,副作用をIOで閉じ込めることができる.

といったことや

Haskellは純粋だけど,理論背景は難しいから副作用を扱えると考えて良いよ.

と言ったことだ.ふむ,なるほどよく分からない.さらには,

Haskellは,純粋な式かどうかがIOという型を見るだけで分かるんだ.どうだい素晴らしいだろ?

と言ったことが書かれている.残念ながら最後のは全くのデタラメだ.さらに検索してみると

IO aとは命令書を意味するデータ型だ.

ふむ,この言説はイメージしやすい.多くの人はこの意見に賛同するだろう.しかしながら,私はこれらとは違った考えを持つ.私は以下の意見が非常に的を得ていると思う.

https://twitter.com/GabrielG439/status/868502950410424321

Haskell has effects. The difference is that they're first-class values and not "to the side"

Haskellは作用を持つ.ただそれらが第1級の値であり,「副」ではないだけさ

さっぱり意味が分からないって?そりゃそうさ.Haskellerは分かってる人には的確な言葉を送れるが,分かってない人に分かるよう説明するのはあまり得意ではないからね.でも,これがたった1つの真実だと僕は思うよ.うーんでもこの感覚を正確に説明するのは難しい.それでも僕の拙いポエムを最後まで聞いてくれる気があるかい? まあ,せっかく綴ったなら1人ぐらいは観客がいてくれると嬉しいね.10人は多すぎるけど.

副作用(side effects)とHaskell

ところで副作用ってなんだろう? 薬を選ぶ時にこの言葉には敏感になるね.なんで敏感になるかを思い出して欲しいんだ.薬を選ぶ時,まず私たちが気にするのは効用,つまり主の作用だ.症状にどれぐらい効くかってことだね.ではなぜ副作用を気にするんだい? それは副作用ってのは今悩んでる問題とは全く別のことをやるからさ.しかも時には病原菌よりタチが悪い.どういう作用を起こすのかぐらいは知っておきたいだろう.でも,大体はパッケージに記載が無かったりするけどね.

もう少し抽象的にこの言葉を考えてみよう.副作用のイメージ図を描いてみた.よく見る図だね.

副作用のイメージ図

副作用ってのは色んな形をしている.例えば,明示的な入力以外のどこかに魔法の壺があって,そこからはランダムに入力を増やせたり,出力についても同じだ.出力の一部が突然虚空に消えてしまい,すっかり忘れた頃に空から降ってくるんだ.それだけじゃない,例えば関数を呼び出すと近所のうるさい犬が突然静かになるんだ.でも関数をさらに2回呼び出すとまたワンワンと吠え出す.

プログラミングでは副作用は,重要な要素だ.そしてそれはHaskellでもそうさ.でもHaskellと他の言語では少し事情が違うんだ.Haskellの関数はみんな次のようなものになっている.

Haskellの関数のイメージ図

unsafeがつく関数とか一部例外はあるけれど,Haskellの全ての関数はみんなこうさ.でもこれでは困ったことになる.副作用のイメージ図はどうやったって上の図にマッチしそうにないからね.じゃあどうすればいいかって? そんなの決まってる.Haskellでは副作用をデータ型にエンコーディングするんだ.非決定的な計算をリストを返す関数にエンコーディングしたり,2つの引数を受け取る関数をカリー化して1つの引数を受け取る関数にエンコーディングしたりするようにね.具体的に例を見せよう.

状態を伴った計算を題材に扱ってみよう.例えば,以下のようなCプログラムで表せるものさ.

int toy_compute(int s) {
  if (s == 0)
    s = 3;

  if (s % 2 == 0)
    s = s + 5;

  if (s % 3 == 1)
    s = 2;

  return (s + 1);
}

これは仮引数sを使って破壊的代入を伴いながら,結果を出す関数だ.これと同様の計算をHaskellで書いてみると,次のようになる.

import Control.Monad
import Control.Monad.State

toyCompute :: State Int Int
toyCompute = do
  get >>= \s ->
    when (s == 0) $ do
      put 3

  get >>= \s ->
    when (s `mod` 2 == 0) $ do
      put (s + 5)

  get >>= \s ->
    when (s `mod` 3 == 1) $ do
      put 2

  get >>= \s ->
    return (s + 1)

そう,単純にStateを使えばいい.そして,Cでtoy_compute(2)って実行するように,HaskellではevalState toyCompute 2って書くことができる.Stateは状態を伴った計算をエンコードできるものだけど,その実態は次のようなものさ.

newtype State s a = State { runState :: s -> (a, s) }

evalState :: State s a -> s -> a
evalState = fst . runState

そうただの関数型s -> (a, s)newtypeなんだ.つまり,上のtoyComputeはまるで状態を更新していってるように見えるけど,その実は単に今の状態を受け取って新しい状態を返してるだけなんだ.そして状態とともに計算値も返すようにしてる.evalState toyComputetoyComputeが状態を受け取って新しい状態と計算値を返してくるから,そのうちの計算値の方だけを持ってくるようにするだけってわけさ.

ところでtoyComputeは具体的な構造を要請してるわけじゃない.get/putに相当するものが存在するならどんなエンコードだっていいんだから.そういう場合は,HaskellだとStateじゃなくてMonadStateを使うんだ.以下の感じにね.

{-# LANGUAGE FlexibleContexts #-}

import Control.Monad
import Control.Monad.State

toyCompute :: MonadState Int m => m Int
toyCompute = do
  get >>= \s ->
    when (s == 0) $ do
      put 3

  get >>= \s ->
    when (s `mod` 2 == 0) $ do
      put (s + 5)

  get >>= \s ->
    when (s `mod` 3 == 1) $ do
      put 2

  get >>= \s -> return (s + 1)

これでStateじゃない構造についても,Stateのインターフェースが使えるようになる.じゃあ,早速だけど別の構造を作ってみよう.Stategetputで随時新しい領域を作成していくんだ.なんたって純粋関数だからね.でも,別に同じ領域を使いまわしたっていい.例えば次のようにIORefを使ってね.

import Data.IORef

newtype StateIORef s a = StateIORef { unStateIORef :: IORef s -> IO a }

evalStateIORef :: StateIORef s a -> s -> IO a
evalStateIORef (StateIORef f) x = newIORef x >>= f

runStateIORef :: StateIORef s a -> s -> IO (a, s)
runStateIORef st = evalStateIORef $ (,) <$> st <*> get

instance Functor (StateIORef s) where
  fmap f (StateIORef g) = StateIORef $ fmap f . g

instance Applicative (StateIORef s) where
  pure x = StateIORef $ \_ -> pure x

  mf <*> mx = StateIORef $ \sr -> do
    f <- unStateIORef mf sr
    x <- unStateIORef mx sr
    return $ f x

instance Monad (StateIORef s) where
  mx >>= f = StateIORef $ \sr -> do
    x <- unStateIORef mx sr
    unStateIORef (f x) sr

instance MonadState s (StateIORef s) where
  get = StateIORef $ \sr -> readIORef sr

  put x = StateIORef $ \sr -> writeIORef sr x

実際のHaskellプログラミングだったらこういう構造は慎重に考えなきゃいけないけど,今回は分かりやすいようにStateIORefを単純に作った.この構造は性能上いくつか問題があるけど,同じ領域を使い回すStateのインターフェースを持った構造ではある.これを使えば,toyComputeは破壊的変更を実行時に行うようにできる.evalStateIORefIOデータに設置することによってね.Haskellは破壊的変更をサポートしてないから,破壊的変更を行うことはできない.でも,破壊的変更作用をIOにエンコードできる.StateIORefは状態を伴った計算を表していて,それは最初の状態を受け取って,破壊的変更作用を表すようなIOデータを返す関数にエンコードすることができるんだ.しかもそれはパーフェクトに純粋な操作だ.文字列を受け取って文字列を返すのと,大体同じさ.

Haskellの関数そのものにエンコードしたかったら私たちはStateを構造に選ぶことができる.もし破壊的変更を伴うものにしたいんだったらStateIORefのようなものを使うことができるし,STRefを使って同じようなこともできるんだ(パフォーマンスに関しては,ちゃんと計測をしてみることをお勧めするけどね).ここで言いたいことは,Haskellは純粋な関数しかないけど,副作用のある計算っていうのは見方を変えればちゃんとデータに落とし込めるようになってるってことさ.そしてそれをどう翻訳するかなんてプログラマの自由ってわけさ.Haskellはそれがどう動くかよりどういうものかを重視するし,どう動くかなんて動かす時に考えればいい.状態を伴う作用っていうのはMonadStateであれば表すことができるし,その作用をどう実行するかなんて考えなくてもいい.実行したい時にそれを満たすような実行する仕組みを作ればいいのさ.ほとんどの場合は,既に誰かが作ってるだろうけどね.

外部とのやり取り

IOが副作用を表すデータという表現をよく見るが,僕はこの表現はあまり好きではない.だって見ただろう? MonadStateも副作用をエンコードしているじゃないか.IOもやっぱりある種の副作用をエンコードしたデータ型の1つさ.それは,Haskell外部とのやり取りを表す作用を一挙に担うんだ.IOが表す作用は,その多くがHaskellでは表現できない.Haskellが表現できないものはたくさんある.例えば,破壊的変更操作とかネットワーク通信とかその大元のFFIとかさ.そういった操作をIO作用が担うんだ.それらはHaskellでは表現できないから,操作の返り値はユニット(())や文字列(String)といったものになるけど,それらにIO作用も付随するようになる.

そうだな,IOの正体を知りたかったらそれをどうやって実現するかを考えてみればいいのさ.IOは僕ならこう実装するね.まずはHaskell上にIOをただのデータ型として定義しておく.IO作用がPutStrLnGetLineしかないのは大目に見てね.

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE GADTs             #-}

import Prelude ( String, Functor(..)
                , Applicative(..), Monad(..)
                , (.)
                )

import Control.Monad

data IO a where
  IOReturn :: a -> IO a
  IO :: IOCommand b -> (b -> IO a)

data IOCommand a where
  PutStrLn :: String -> IOCommand ()
  GetLine  :: IOCommand String

instance Functor IO where
  fmap f m = m >>= return . f

instance Applicative IO where
  pure = IOReturn
  (<*>) = ap

instance Monad IO where
  IOReturn x >>= f = f x
  IO cmd f >>= g = IO cmd $ \x -> f x >>= g

primCommand :: IOCommand a -> IO a
primCommand cmd = IO cmd pure

putStrLn :: String -> IO ()
putStrLn str = primCommand $ PutStrLn str

getLine :: IO String
getLine = primCommand GetLine

それからC言語でHaskellを実行する部分を書こう.C言語の方は収まりきる長さじゃないし,実装する時間もないからアイデアだけを置いておくね.そして,出現する文字列は全て128文字以下だとしておこう.

#include <stdio.h>
#include <stdbool.h>

#define MAX_BUFFER_SIZE (128 + 1)

typedef struct HaskellValue {
  ...
} HaskellValue;

typedef struct HaskellIOExpr {
  ...
} HaskellIOExpr;

enum IOCommandTag {
  PutStrLn,
  GetLine,
};

typedef struct PutStrLnData {
  char message[MAX_BUFFER_SIZE];
} PutStrLnData;

typedef struct IOCommand {
  IOCommandTag tag;
  void* data;
} IOCommand;

void init_expression(HaskellIOExpr *expr);
bool is_io_return(const HaskellIOExpr *expr);

void compute_haskell_io_expression(HaskellIOExpr *expr);
void set_haskell_unit_value(HaskellValue *val);
void set_haskell_string_value(HaskellValue *val, const char *str);

IOCommand *get_io_command(const HaskellIOExpr *expr);
void free_io_command(IOCommand *io_cmd);

void apply_and_update_io(HaskellIOExpr *expr, const HaskellExpr *value);

void io_action(HaskellIOExpr *expr, const IOCommand *io_cmd) {
  HaskellVal app_val;

  switch (io_cmd->tag) {
    case PutStrLn:
      printf("%s\n", ((PutStrLnData)io_cmd->data).message);
      set_haskell_unit_value(&app_val);
      break;
    case GetLine:
      char getline_buffer[MAX_BUFFER_SIZE];
      gets_s(getline_buffer, MAX_BUFFER_SIZE);
      set_haskell_string_value(&app_val, getline_buffer);
      break;
  }

  apply_and_update_io(expr, &app_val);
}

int main(void) {
  HaskellIOExpr expr;
  init_expression(&expr);

  for (IOCommand *io_cmd;;) {
    compute_haskell_io_expression(&expr);
    if (!is_io_return(&expr)) {
      break;
    }

    io_cmd = get_io_command(&expr);
    io_action(&expr, io_cmd);
    free_io_command(io_cmd);
  }
}

...

... で示したところの実装はそれほど難しくないはずさ.そこの部分をちゃんと書けば,StateがただのHaskell上の式に,StateIORefIOデータに接地できたように,IOデータはなんとC言語プログラムに接地できる.後は完成したプログラムをコンパイルしてやればそいつがIOデータを動かしてくれるrunIOのような関数の役割を担ってくれるってわけさ.

実際のところ,現実にあるHaskellの実装はもっともっと賢いことをやっている.ただ発想が重要なんだ.上のCプログラムにはその全てを詰めたつもりさ.IOデータはHaskell上でなんということはないただのデータとして表現できたし,これは完璧に純粋で通常のHaskellのデータ型と同じだ.そしてHaskellのmain関数は単なるIOデータというわけさ.

結局IOデータは副作用を表すんじゃないかって? おいおい,ここまできてよしてくれよ.じゃあIOの副作用ってなんだい? 上の実装はパーフェクトにIOデータをただのデータ型として表現している.Cプログラムは確かに不純で,標準のI/Oバッファの状態によって挙動が180度変わることもあるだろう.じゃあ例えば,標準のI/Oバッファの挙動が完璧に定義されていて,他のプロセスからの干渉を一切受けず,いつでも同じ状態から始まり,いつでも同じ入力を受けるとしたらどうだい? それは副作用をもつだろうか? だって上のCプログラムはいつ実行しても全く同じ挙動になるんだ.少なくとも純粋なプログラムにはなるだろう.

MonadStateの時を思い出して欲しい.あの時私たちはただの関数にある解釈を与えることで,まるで変更可能な大域変数と共に計算しているようなプログラムを書けた.それは単なるStateというデータに対する解釈では擬似的なものだったけど,StateIORefの方は完全に変更可能な大域変数と共に動くプログラムを書いていた.IOも同じことさ.私たちが記述したのはどういったIO作用を起こすかどうかだけってわけさ.実際にその作用を翻訳するのは上のCプログラムのようなものさ.これを私たちはランタイムと呼んでいる.現実世界っていうのは突拍子もないことがよく起こるし,情報量過多でもある.そんなんだから不純なプログラムを動かしたくもなるさ.でも,もしあなたがランタイムすら完全に純粋であることを望むなら,io_actionから不純な行いを消せばいいんだ.入力は常に"42"で出力は常に虚空に消えるようにしておけばいい.そうしたら,満足な結果が得られるだろう.

GHCではIOの中身はもっと明確で扱いやすいものになっている.残念だけど,ちょっと長くなるので,ここでは解説しない.ただ押さえて置いて欲しいのは,GHCのIOもやっぱり単なるデータ型だということなんだ.私たちにはIOの中身は見れないけれど,それは不思議でもなんでもない.だってコンストラクタの隠蔽なんてよくやることじゃないか.単に型だけが公開されているだけさ.HaskellではIOの実装方法は各処理系に任されている.だから,処理系依存にしないためにも中身を公開していないだけだし,現にHaskellを書いてる時IOの中身が公開されていないことで困ったことはないだろう? 中身はどうでもよくはないけど,大事なことはIOが単なるデータ型で,外部とのやり取りを伴うような作用を表現しているデータの型という,ただそれだけってわけなのさ.

副作用再考

便宜上,私はこのポエムの中で副作用って言葉を使ってきたけど,実はあまりこの言葉は使いたく無かったんだ.最初の出だしを見てくれれば分かるけど,この話は「Haskellには作用がある.でもそれは第1級の値で,『副』ではないよ」というところから始まった.第1級の値であることはよく分かったでしょ? だって,StateIOも単なるデータだし,それはHaskell内でいじり倒し放題だ.以下のようにね.

chooseIOEffect :: Bool -> IO a -> IO a -> IO a
chooseIOEffect True  _ mx = mx
chooseIOEffect False mx _ = mx

この関数は2つIOがあった時,どっちのIO作用にするかを引数で選ぶ関数だ.どっちかを選んだらもう片方の作用は虚空に消える.もし消したくなくて,両方を含みたいなら次のように書けばいい.

chooseIOEffectOrder :: Bool -> IO a -> IO a -> IO a
chooseIOEffectOrder True  mx my = mx >> my
chooseIOEffectOrder False mx my = my >> mx

もちろん,作用は一緒でどっちの結果を返すかだけ変えることだってできる.

chooseReturnValue :: Bool -> IO a -> IO a -> IO a
chooseReturnValue b mx my = do
  x <- mx
  y <- my
  return $ if b then x else y 

作用をどう組み合わせるかなんて,Haskellではちょちょいのちょいさ.だってHaskellでは作用ってのは単なるデータなんだからね.注文が来たらそれ通りのオーダーで作用をコーディネートすればいいのさ.do構文で飾り付けをしてやれば,関数プログラミングが苦手な顧客も大喜びでチップをはずんでくれるだろう.

作用が第1級というのはよく分かった.でも,「副」じゃないってのはどういうわけだろう? これはちょっと考えて見ればすぐ分かることなんだよ.だって,chooseIOEffectという関数の返り値はどういう型を持ってるだろう? もっとわかりやすい身近な例で言えば,putStrLn :: String -> IO ()の返り値の型はなんだい? あなたはバカにされてるんじゃないかって思うかもしれない.そんなのはIO ()に決まってるってね.でもこれはとても大事なことなんだ.つまりね,()ではないんだよ.IO ()なんだ.IO ()という1つのデータ型なんだよ.私たちがputStrLnという関数を使う時,その関数が返してくる値として期待するのは()型の値ではなく,IO ()型の値なんだ.だってそうだろう? もしputStrLnString -> ()という型の関数で,出力したいメッセージを渡したのに,そのメッセージを無視してただ情報量が全くない()を返して来たら多くの人が怒るだろう.つまりね,私たちがputStrLnを使う時に期待するのは「メッセージを出力する」というIO作用そのものなんだよ.だから期待通り,HaskellではputStrLnString型の値を渡すとIO ()という型のデータを返してくるんだ.「メッセージを出力する」ってのが副作用だって? 誰が()型の値が欲しくってputStrLnを呼び出すというんだい? 「メッセージを出力する」はむしろ主作用なんだよ.まさに私たちはそれが返ってくることを期待してるんだ.

そう,StateIOというのはそれほど難しいことを表してるわけじゃないんだ.単にそういう作用を表す型っていうだけなんだよ.副作用を閉じ込めるとか,何か難しい理屈で動いてるとか,命令書を表すとかそういうのじゃないんだ.put :: s -> State s ()というのは更新したい値を受け取って状態をその値で更新するよという作用を返してくるだけだし,putStrLn :: String -> IO ()は出力したいメッセージを受け取ってそれを標準出力するよという作用を返してくるだけなんだ.作用はそれぞれState s ()IO ()というデータで表せるからHaskellは律儀にそれを守ってくれてるだけなんだよ.だって,それが普通のHaskellプログラミングというものだろ? 表現したいものに実際の構造と型を与えて,その操作を組み立てていくのがHaskellプログラミングだ.作用も単にその1つってだけなんだよ.

Haskellの作用は,「副」じゃないんだ.だってちゃんと型で守られているからね.泣いても笑っても,Haskellはそのデータがそういう型を持ってると言われたらそれに律儀に従う.もし,私たちが作用をどこからか取得して来たら,それにはちゃんとその作用を表す型がついてるんだ.そこに副なんてない.まあ見方次第だけどね.関心のある場所は取得して来たもののたった一部分というのは,よくあることだからね.作用に関しても,そういうことはよくあるかもね.でもそれは関心のあるデータの序列の問題だ.それで副作用という言葉を使うなら,putStrLn :: String -> IO ()()型のデータは副データというべきだね.もっと一般的には,副部分構造かな.まあでも私が言いたいのは,副作用っていう言葉はHaskellではその程度の意味ってことなのさ.

だから最後にもう一度引用しておこう.

https://twitter.com/GabrielG439/status/868502950410424321

Haskell has effects. The difference is that they're first-class values and not "to the side"

Haskellは作用を持つ.ただそれらが第1級の値であり,「副」ではないだけさ

最後に

最後まで読んでくださった方ありがとうございました.ただし,1つ押さえて置いて欲しいことは,これは私の考えであって,なんのソースもなく形式的な話もしていないということです.まあ,単なるポエムなので共感するもしないもあなたの自由だということです.ただ,もしこのポエムであなたがHaskellに興味を持ってくれたなら,それが何より嬉しいことでしょう.

さて,この記事は形式的な話を排除したのを良いことに,話がとても曖昧です.最後にもう少し形式的な話についてと,Haskell以外の言語について少々触れておきたいと思います1

この記事では作用とは何か,そもそもどこの流れを組んだ話なのかを全くしてきませんでした.まあそこらへんはポエムなので,許してください.この話は関数プログラミング界隈では結構昔から活発に研究が行われている,「計算(computation)」の話を汲むものです.この話に関しては,ちゃんとした人たちが幾つか話をまとめてくれているので,特に書く意味がないのですが,一応ざっとしたまとめを書いておきます.ついでに勘違いしないで欲しいのですが,今回の記事の主眼は「Haskellでの計算作用の考え方のイメージを自分なりに伝えること」であり,「計算作用を如何に扱いやすくするか」や「モナドとは何か」(本文中でモナドという言葉を扱った覚えはありません)ではありません.そこのところをよろしくお願いします.

計算とは,様々な形をしています.非決定的であったり,副作用を持っていたりです.例えば,1 == select(0, 1)というプログラムにおいて,select関数は非決定的で01どちらかの値を流してくるとします.このような意味論を持つプログラミング言語があったとすると,その意味を単純にブール値の値に翻訳するのはナンセンスです.[Moggi91]は,このようなプログラムの意味論にクレイスリ・トリプルの構造を充てがうことを提唱した,古典的な論文です.これは計算をうまく扱うために,プログラムの意味論をどう設計するかに着目したものですが,Wadlerがその後,[Wadler92]で通常の関数プログラミングの範囲において,様々な種類の計算に扱いやすいインターフェースを与えることを目的として,クレイスリ・トリプルを使用することを提案しました.この論文は計算によって起きる作用に注目し,その作用が制御に大きく影響を与える場合に,クレイスリ・トリプルを使用するといった趣旨のものです.これが現在のHaskellの体制に大きく影響を与えています.ここから発展してエフェクトシステムというものが提案されました.近年の論文としては[Plotkin09]に基本的なアイデアがあります.依存型との連携に関して[Brady13]などもあります.関連する論文はEffectPapersにまとまってるので,詳細に興味がある方は,古い順から読んで見るといいかもしれません.ただ,もっときちんと解説された日本語記事として,プログラミングにおけるモナドの初期の歴史についてという記事や,みょんさんのエフェクトシステムの解説(こっちは結構解説が大雑把なので,[Benton02]とかEffのチュートリアル論文とかを読んでから,俯瞰記事として読んだ方がいいかもしれない)とかもあるので,まずはそちらを当たってみることをお勧めします.

さて外部とのやりとりを扱うことは,プログラミングでは非常に重要なトピックですから,様々な言語で異なるアプローチの作用を扱う仕組みが用意されています.Haskellはmain関数を,特別な外部とのやり取りを伴う作用IO aで定義するという道を選択しましたが,例えばConcurrent Clean[Clean22]という言語は,少し異なるアプローチをとる言語です.この言語は,ある種の性質を持つ関数を区別する型システムを導入することで,main関数を通常のCleanの範囲で定義する関数として書けるようにしています.以下が,Cleanのコードとなります2:

module helloworld

import StdEnv

fputline :: String *File -> *File
fputline str f
  # f = fwrites str f
  # f = fwritec '\n' f
  = f

Start :: *World -> *World
Start world
  # (console, world) = stdio world
  # console          = fputline "Hello, World!" console
  # (ok, world)      = fclose console world
  | not ok           = abort "Cannot close console.\n"
  | otherwise        = world

*という印がついてる型はそれが一意的,つまり一度しか使えないことを示しています.Startは単なる一意性を持った関数として定義されており,Haskellのような特別なデータ構造で定義する必要はありません.このように外部とのやり取りを単によくある関数で表現できるところが,一意型システムの面白いところです.

また,PureScript[PureScript]ではIOの代わりにEffというものを用いていました.いましたというのは,次期メジャーリリース(0.12.0)からは,Haskellと同じくIO作用によってmain関数を定義することを選択したからなんですが.Effはとても単純で,c = pure 1 :: Eff e Intというものを次のようなJavaScriptコードと同様のものに変換します3:

function c() {
  return 1;
}

この定数を参照する際はcall-by-nameのように実行されることになります.つまり,作用を関数として表現することで,関数の実行を行うまで作用の翻訳を遅らせるというわけです.Effはrow typesという特徴的な仕組みも搭載しているのですが,これは複雑なシステムの割にメリットがあまり無いというのが,IOへの移行という方針転換に至った経緯のようです.

作用に関する話題は様々で,他にも多くのトピックがありますが,今回はこれくらいにしておきましょう.もし,興味があれば色々調べて見ると面白いかもしれません.

それから@whywaita,記事書いたからなんかおいしいものおごって

参考文献

  • [Moggi91] Eugenio Moggi, Notions of computation and monads, Information and Computation 93 (1991), Issue 1, 55-92
  • [Wadler92] Philip Wadler, The Essence of Functional Programming, Proceedings of the 19th POPL (1992). 1-14
  • [Benton02] Nick Benton, John Hughes, and Eugenio Moggi, Monads and Effects, International Summer School on Applied Semantics 2000. Volume 2395 of Lecture Notes in Computer Science. (2002), 42–122
  • [Plotkin09] Plotkin Gordon, and Pretnar Matija, Handlers of Algebraic Effects, Proceedings of the Programming Languages and Systems: 18th European Symposium on Programming (2009), 80-94
  • [Brady13] Edwin Brady, Programming and reasoning with algebraic effects and dependent types, SIGPLAN Not. 48 (2013), no. 9, 133-144
  • [EffectPapers] Effects bibliography, https://github.com/yallop/effects-bibliography
  • [Haskell10] Haskell 2010 Language Report, https://www.haskell.org/onlinereport/haskell2010/.
  • [Clean22] Concurrent Clean Version 2.2 Language Report, http://clean.cs.ru.nl/download/html_report/CleanRep.2.2_1.htm.
  • [PureScript] PureScript Documentation, https://github.com/purescript/documentation/

  1. 僕はそんなに詳しく無いので,何か指摘があればどんどんしてもらえるとありがたいです. 

  2. 残念ながら自分の環境ではCleanのコンパイラが動かなかったので,動作確認はしていません.何かコードに間違いがあれば,指摘をもらえるとありがたいです. 

  3. 実際には var c = Control_Applicative.pure(Control_Monad_Eff.applicativeEff)(1);というコードに変換されます.Effモナドのpureの実装は https://github.com/purescript/purescript-eff/blob/4f55713a012a578663beabaf0368cccf946af449/src/Control/Monad/Eff.js#L3 というものになっているため,最終的に同じ動作をすることになります. 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away