「Haskellは純粋関数型言語だから副作用がない」っていうの、そろそろ止めにしませんか?

YAMAMOTO Yuji (山本悠滋)

2025-06-15 関数型まつり 2025

はじめまして! 👋😄

🇯🇵自己紹介 + 宣伝 1:

🔰自己紹介 + 宣伝 2:

📝今日話すこと

⚠️おことわり

どうして「純粋」なのか

Haskell が生まれた背景

Haskell 以前の IO

Haskell以前における純粋関数型言語の代表、Miranda最新版(2020年リリース‼️)マニュアル曰く…

Haskell 1.0 - 1.2 の IO (1)

Report on the Programming Language Haskell Version 1.0より:

Haskell’s I/O system is based on the view that a program communicates to the outside world via streams of messages:

type Dialogue = [Response] -> [Request]

Haskell 1.0 - 1.2 の IO (2)

Report on the Programming Language Haskell Version 1.0より:

Responseのリスト(ストリーム)を受け取って、Requestのリスト(ストリーム)を返す関数

type Dialogue = [Response] -> [Request]

Haskell 1.0 - 1.2 の IO (3)

Haskell 1.0 - 1.2 の IO (4)

data Request =
    ReadFile String
  | WriteFile String String
  | ReadChan String
  | AppendChan String String
  | GetEnv String
  | SetEnv String String
  | ...

data Response =
    Success
  | Str String
  | Failure IOError
  | ...

Haskell 1.0 - 1.2 の IO (5)

Haskell 1.0 - 1.2 の IO (6)

Dialogueを使って書いたコードの例
 

ソースコードを画像化したもの。元のソースコードは https://github.com/igrep/igreque.info/blob/master/slides/2025-06-15-fp-matsuri.md?plain=1#L133-L143 周辺を参照

Haskell 1.0 - 1.2 の IO (7)

😩引数でパターンマッチした種類のResponseと、対応するRequestの順番が一致してないとダメ!

前のソースコードを画像化してResponseとRequestの対応関係が分かるよう矢印を付けたもの。

Haskell 1.0 - 1.2 の IO (8)

⏩使いにくいので継続渡しによるラッパーが

readFile name
  (\msg -> errorTransaction)
  (\contents -> successTransaction)

Haskell 1.0 - 1.2 の IO (9)

⏩継続渡しって?

Haskell 1.0 - 1.2 の IO (10)

⏩継続渡しって?

比較的身近な例: JavaScriptのコールバックを受け取る関数

// Node.jsのfsモジュール
readFile('file.txt', (err, data) => {
  console.log(data);
});
// Promiseも立派な継続渡し
// then メソッドが継続を受け取る
readFile(filePath).then((contents) => {
  console.log(contents);
});

Haskell 1.0 - 1.2 の IO (11)

⏩継続渡しに変換すると型はどう変わる?

// 継続渡しじゃない普通の関数(型定義はTypeScriptの構文)
foo(input: number): string;

// その継続渡しバージョン
foo(input: number, k: (output: string) => void): void

Haskell 1.0 - 1.2 の IO (12)

先程のプログラムをcontinuation basedで書き換えたもの

main =
  appendChan stdout "please type a filename\n" exit (
    readChan stdin exit (\userInput ->
      let (name : _) = lines userInput in
        appendChan stdout name exit (
          readFile name
            (\ioError ->
              appendChan stdout
              "can't open file" exit done)
            (\contents ->
              appendChan stdout contents exit done))))

Haskell 1.3以降の IO (1)

Report on the Programming Language Haskell Version 1.3より:

The I/O system in Haskell is purely functional, yet has all of the expressive power found in conventional programming languages. To achieve this, Haskell uses a monad to integrate I/O operations into a purely functional context.

Haskell 1.3以降の IO (2)

先程のプログラムを現代のHaskellに(ほぼ)直訳したもの

main = do
  putStrLn "please type a filename\n"
  userInput <- hGetContents stdin
  let (name : _) = lines userInput
  putStrLn name
  contents <- readFile name
  putStrLn contents

IO は純粋な関数?

IOの正体: それでも純粋っぽい部分 (1)

IOの正体: それでも純粋っぽい部分 (2)

IOの正体: それでも純粋っぽい部分 (3)

IOの正体: それでも純粋っぽい部分 (4)

>>=の例1: IO同士を繋げる

getLine >>= (\line -> putStrLn line)

do記法で分かりやすくしたバージョン:

do
  line <- getLine
  putStrLn line

やっぱりこれはダメ:

do
  let line = getLine()
  putStrLn line

>>=の例2: 純粋な関数の中からIOを呼んでIOにする (1)

純粋な関数

addOne :: Int -> Int
addOne x = x + 1

>>=の例2: 純粋な関数の中からIOを呼んでIOにする (2)

その中でIOを呼ぶ

addOne :: Int -> IO Int
addOne x = print x >>= (\_unused -> return (x + 1))

>>=の例2: 純粋な関数の中からIOを呼んでIOにする (3)

do記法で分かりやすくしたバージョン:

addOne :: Int -> IO Int
addOne x = do
  print x
  return (x + 1)

IOを繋げて「組み立てる」だけ? (1)

それを言ったら、他の言語も「命令を列挙している」だけでは?

// JavaScript風の擬似コード
function main() {
  print("please type a filename");
  const userInput = getInput();
  const name = userInput.split("\n")[0];
  print(name);
  const contents = readFile(name);
  print(contents);
}

IOを繋げて「組み立てる」だけ? (2)

想定反論: こういう意見もある

何故こんな議論に?

まとめ