Open25

Study Haskell 2025

mizchimizchi

久しぶりに(10年ぶりに?)Haskell書きたくなったので環境構築していく。
TS がふわふわ言語仕様過ぎて、AIに好きに書かせると崩壊する。コンパイルの難易度が高くても型の表現力が高いHaskellでどこまでやれるかをテストしたい。
(今回 Rust でないのは、システムレベルではない意味論抽象に主にフォーカスしたいから。 具体的には Extensible Effects を使いたい)

初手でこういうプロンプトを書いた。

私は haskell のエキスパートで、ユーザーに haskell の使い方を教えながらコードを書きます。

ユーザーは TypeScript のエキスパートで、多少のRustの知識があります。Haskell は初心者です。

このプロジェクトは最新の cabal でセットアップされています。
stack を使わず、 cabal のみを使ってビルドと依存関係の管理を行います。

インストール

ghcup で全部を管理する。今の stack はメンテナンスモードで、 cabal 自体が進化したので使う必要はないらしい。
とりあえず vscode で hls(LSP) が動くことを目標に環境を作る。

$ curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh

.zshrc に追記

export PATH=$PATH:$HOME/.ghcup/bin
[ -f "$HOME/.ghcup/env" ] && source "$HOME/.ghcup/env" 

20250413現在、9.10.1 が安定版なので、これに合わせて cabal, hls, ghc を設定する

$ ghcup tui
# or
$ ghcup install ghc 9.10.1
$ ghcup install cabal 3.14.2.0
$ ghcup install hls 2.9.0.1
$ ghcup set ghc 9.10.1

プロジェクトのセットアップ

$ mkdir helloworld
$ cd helloworld
$ cabal init

# checkout cabal
$ curl -s -L https://stackage.org/lts/cabal.config -o cabal.project.freeze
# cabal.project.freeze のGHCバージョンを 9.10.1 に書き換える
$ cabal update
$ cabal freeze

vscode 拡張をいれる
https://marketplace.visualstudio.com/items?itemName=haskell.haskell

LSP を有効にするのに、 settings.json に自分の使ってるバージョンを記入

.vscode/settings.json
{
  "haskell.manageHLS": "GHCup",
  "haskell.toolchain": {
    "ghc": "9.10.1",
    "hls": "2.9.0.1",
    "cabal": "3.14.2.0"
  }
}

自分の場合はこれで LSP が補完されるようになった

mizchimizchi

ディレクトリ名と同名の *.cabal を作る必要がある。
stack だと project.yaml を hpack で *.cabal に変換していたが、stack を使わないので .cabal を直接編集する。

こういう状態

$ tree app src test helloworld.cabal     
app
└── Main.hs
src
└── Lib.hs
test
└── Spec.hs
helloworld.cabal
cabal-version: 2.2
name: helloworld
version: 0.1.0.0
description: wip
author: mizchi
maintainer: miz404@gmail.com
copyright: 2025 Author name here
license: MIT
license-file: LICENSE
build-type: Simple
extra-source-files:
  CHANGELOG.md
  README.md

source-repository head
  type: git
  location: https://github.com/githubuser/helloworld

library
  exposed-modules:
    Lib

  other-modules:
    Paths_helloworld

  autogen-modules:
    Paths_helloworld

  hs-source-dirs:
    src

  ghc-options:
    -Wall
    -Wcompat
    -Widentities
    -Wincomplete-record-updates
    -Wincomplete-uni-patterns
    -Wmissing-export-lists
    -Wmissing-home-modules
    -Wpartial-fields
    -Wredundant-constraints

  build-depends:
    base >=4.20 && <5,
    cabal-gild,

  build-tool-depends: cabal-gild:cabal-gild >=1.6.0.0
  default-language: Haskell2010

executable helloworld-exe
  main-is: Main.hs
  hs-source-dirs:
    app

  ghc-options:
    -Wall
    -Wcompat
    -Widentities
    -Wincomplete-record-updates
    -Wincomplete-uni-patterns
    -Wmissing-export-lists
    -Wmissing-home-modules
    -Wpartial-fields
    -Wredundant-constraints
    -threaded
    -rtsopts
    -with-rtsopts=-N

  build-depends:
    base >=4.20 && <5,
    helloworld,

  default-language: Haskell2010

test-suite helloworld-test
  type: exitcode-stdio-1.0
  main-is: Spec.hs
  other-modules:
    Paths_helloworld

  autogen-modules:
    Paths_helloworld

  hs-source-dirs:
    test

  ghc-options:
    -Wall
    -Wcompat
    -Widentities
    -Wincomplete-record-updates
    -Wincomplete-uni-patterns
    -Wmissing-export-lists
    -Wmissing-home-modules
    -Wpartial-fields
    -Wredundant-constraints
    -threaded
    -rtsopts
    -with-rtsopts=-N

  build-depends:
    base >=4.20 && <5,
    helloworld,

  default-language: Haskell2010

library でこのライブラリとしての参照を登録して、これを helloworld として build-depends に追加する。

cabal run Main.hs が実行され、 cabal test でテストが走る状態

GitHub CI

CI を回しておく

.github/workflows/ci.yaml
name: Haskell CI

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-haskell@v1
        with:
          ghc-version: "9.10.1"
          cabal-version: "3.14.2.0"

      - name: Cache
        uses: actions/cache@v3
        env:
          cache-name: cache-cabal
        with:
          path: ~/.cabal
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-
      - name: Install dependencies
        run: |
          cabal update
          cabal build --only-dependencies --enable-tests --enable-benchmarks
      - name: Build
        run: cabal build --enable-tests --enable-benchmarks all
      - name: Run tests
        run: cabal test all
mizchimizchi

ユニットテストを書く。まずは簡単な関数を書いてツールの使い方を確認する

src/Lib.rs
module Lib (add) where
add :: (Num a) => a -> a -> a
add x y = x + y

ただの足し算の関数を追加した。
test-suite に hunit の依存を追加する。

test-suite helloworld-test
  type: exitcode-stdio-1.0
  main-is: Spec.hs
  other-modules:
    Paths_helloworld

  autogen-modules:
    Paths_helloworld

  hs-source-dirs:
    test

  ghc-options:
    -Wall
    -Wcompat
    -Widentities
    -Wincomplete-record-updates
    -Wincomplete-uni-patterns
    -Wmissing-export-lists
    -Wmissing-home-modules
    -Wpartial-fields
    -Wredundant-constraints
    -threaded
    -rtsopts
    -with-rtsopts=-N

  build-depends:
    HUnit ^>=1.6,
    base >=4.20 && <5,
    helloworld,

  default-language: Haskell2010

これでこの add 関数に対するテストを書く

test/Spec.hs
import Lib
import Test.HUnit
import qualified System.Exit as Exit

test1 :: Test
test1 = TestCase (assertEqual "should return 3 (Int)" (3 :: Int) (add (1 :: Int) (2 :: Int)))

tests :: Test
tests = TestList [
    TestLabel "add test (Int)" test1,
    ]

main :: IO ()
main = do
    result <- runTestTT tests
    if failures result > 0 then Exit.exitFailure else Exit.exitSuccess

実行

$ cabal test  
Configuration is affected by the following files:
- cabal.project.freeze
Build profile: -w ghc-9.10.1 -O1
In order, the following will be built (use -v for more details):
 - helloworld-0.1.0.0 (test:helloworld-test) (configuration changed)
Configuring test suite 'helloworld-test' for helloworld-0.1.0.0...
Preprocessing test suite 'helloworld-test' for helloworld-0.1.0.0...
Building test suite 'helloworld-test' for helloworld-0.1.0.0...
[3 of 3] Linking dist-newstyle/build/x86_64-linux/ghc-9.10.1/helloworld-0.1.0.0/t/helloworld-test/build/helloworld-test/helloworld-test [Library changed]
Running 1 test suites...
Test suite helloworld-test: RUNNING...
Cases: 6  Tried: 6  Errors: 0  Failures: 0
Test suite helloworld-test: PASS
Test suite logged to:
/home/mizchi/sandbox/helloworld/./dist-newstyle/build/x86_64-linux/ghc-9.10.1/helloworld-0.1.0.0/t/helloworld-test/test/helloworld-0.1.0.0-helloworld-test.log
1 of 1 test suites (1 of 1 test cases) passed.

テストが通った。

mizchimizchi

Quicksort を書いてみる。(ほぼAIが補完したが...)

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
    let smallerSorted = quicksort [a | a <- xs, a <= x]
        biggerSorted  = quicksort [a | a <- xs, a > x]
    in  smallerSorted ++ [x] ++ biggerSorted

これに対するテストを追加

Spec.hs
import Lib
import Test.HUnit
import qualified System.Exit as Exit

test1 :: Test
test1 = TestCase (assertEqual "should return 3 (Int)" (3 :: Int) (add (1 :: Int) (2 :: Int)))

-- Double 型での add 関数のテスト
testAddDouble :: Test
testAddDouble = TestCase (assertEqual "should return 3.0 (Double)" (3.0 :: Double) (add (1.5 :: Double) (1.5 :: Double)))

-- クイックソートのテストケース
quickSortTestEmpty :: Test
quickSortTestEmpty = TestCase (assertEqual "for empty list" ([] :: [Int]) (quicksort []))

quickSortTestSorted :: Test
quickSortTestSorted = TestCase (assertEqual "for sorted list" ([1, 2, 3] :: [Int]) (quicksort ([1, 2, 3] :: [Int])))

quickSortTestReverse :: Test
quickSortTestReverse = TestCase (assertEqual "for reverse sorted list" ([1, 2, 3] :: [Int]) (quicksort ([3, 2, 1] :: [Int])))

quickSortTestRandom :: Test
quickSortTestRandom = TestCase (assertEqual "for random list" ([1, 1, 2, 3, 4, 5, 6, 9] :: [Int]) (quicksort ([3, 1, 4, 1, 5, 9, 2, 6] :: [Int])))

-- 全テストをまとめる
tests :: Test
tests = TestList [
    TestLabel "add test (Int)" test1,
    TestLabel "add test (Double)" testAddDouble,
    TestLabel "quicksort empty" quickSortTestEmpty,
    TestLabel "quicksort sorted" quickSortTestSorted,
    TestLabel "quicksort reverse" quickSortTestReverse,
    TestLabel "quicksort random" quickSortTestRandom
    ]

main :: IO ()
main = do
    result <- runTestTT tests
    if failures result > 0 then Exit.exitFailure else Exit.exitSuccess
mizchimizchi

もうちょっとリアルワールドっぽいの。
サーバーにGETリクエストをして、JSONとしてパースする。

プロンプト

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json)) 相当のコードを haskell で書きたい

  build-depends:
    base >=4.20 && <5,
    helloworld,
    req ^>= 3.13,
    aeson ^>= 2.2,
    text ^>= 2.1,
    bytestring ^>= 0.11,

ここだいぶ適当にライブラリバージョンを選んでそうなので、確認が必要

app/main.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveGeneric #-} -- FromJSON と Show の自動導出に必要

module Main (main) where

import qualified Network.HTTP.Req as Req
import qualified Data.Aeson as Aeson
import Control.Monad.IO.Class (liftIO)
import GHC.Generics (Generic) -- DeriveGeneric に必要
import Data.Text (Text) -- Todo 型の title フィールドで使用

-- JSON 構造に対応するレコード型
data Todo = Todo
  { userId :: Int
  , id :: Text
  , title :: Text -- JSON の文字列は Text 型で扱うのが一般的
  , completed :: Bool
  } deriving (Generic, Show) -- Generic と Show を自動導出

-- FromJSON インスタンスを自動導出
-- これにより Aeson が JSON から Todo 型へ変換する方法を理解する
instance Aeson.FromJSON Todo

main :: IO ()
main = Req.runReq Req.defaultHttpConfig $ do
  -- レスポンスボディを Todo 型としてパースする
  response <- Req.req Req.GET
                  (Req.https "jsonplaceholder.typicode.com" Req./: "todos" Req./: "1")
                  Req.NoReqBody
                  Req.jsonResponse -- 型推論により Todo 型を期待する
                  mempty

  -- パースされた Todo 型のボディを取得
  let todo :: Todo = Req.responseBody response

  -- 結果 (Todo 型) をコンソールに出力 (Show インスタンスが使われる)
  liftIO $ print todo
$ cabal run
Todo {userId = 1, id = 1, title = "delectus aut autem", completed = False}
mizchimizchi

たぶん Hackage MCP か Haddock MCP みたいなのが必要

mizchimizchi

Format

Ormole を入れる。 cabal install ormolu

https://github.com/fourmolu/fourmolu

fork の https://github.com/fourmolu/fourmolu というのもあったが、自分には違いがわからなかったのと vscode 拡張がなかったので後回しにする。

{
  "ormolu.path": "/home/mizchi/.cabal/bin/ormolu",
  "ormolu.args": [
    "--no-cabal"
  ],
  "[haskell]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "sjurmillidahl.ormolu-vscode"
  },
}

ormolu 拡張が環境変数に対応してないので、 /home/mizchi/.cabal/bin/ormolu の絶対パス指定で書く必要があった。悲しい。config が共有できなくなった。

この状態で Ctrl-S で保存するとフォーマットされるのを確認

コマンドで流したければこっち

$ ormolu -i **/*.hs
## CI 用
$ ormolu -m check **/*.hs
src/Lib.hs
@@ -123,8 +123,8 @@
              let baseUrl = Req.https (T.pack host)
              let url = foldl (Req./:) baseUrl (map T.pack pathSegments)
              response <-
-               Req.req Req.GET url Req.NoReqBody Req.jsonResponse
-                 $ Req.port port
+               Req.req Req.GET url Req.NoReqBody Req.jsonResponse $
+                 Req.port port
              let todo = Req.responseBody response :: Todo
              return todo
            else do
@@ -133,7 +133,7 @@
              let baseUrl = Req.http (T.pack host)
              let url = foldl (Req./:) baseUrl (map T.pack pathSegments)
              response <-
-               Req.req Req.GET url Req.NoReqBody Req.jsonResponse
-                 $ Req.port port
+               Req.req Req.GET url Req.NoReqBody Req.jsonResponse $
+                 Req.port port
              let todo = Req.responseBody response :: Todo
              return todo
@@ -174,4 +174,4 @@
  quicksort (x : xs) =
    let smallerSorted = quicksort [a | a <- xs, a <= x]
        biggerSorted = quicksort [a | a <- xs, a > x]
-   in smallerSorted ++ [x] ++ biggerSorted
+    in smallerSorted ++ [x] ++ biggerSorted 

自分は indentRainbow というインデントで色を変えてくれる vscode 拡張を愛用しているのだが、それと組み合わせると奇数インデントのハイライト警告でてしまうので、無視する。というかその設定が indentRainbow 側の設定サンプルにあるので念頭にあるのだと思う。
https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow

  "indentRainbow.ignoreErrorLanguages": ["haskell"]
mizchimizchi

hlint

$ cabal install hlint
$ hlint --default > .hlint.yaml

$ hlint src test
No hints

一旦 ormole とコンフリクトしてそうな部分を無視した。

.hlint.yaml
- ignore: {name: Use head}
- ignore: {name: Use guards}

cabal 自体には npm scripts のようなテストランナー相当の機能を持ってなさそうなので、Makefile を書いた

all: build test run

run:
	@cabal run helloworld-exe -- https://jsonplaceholder.typicode.com/todos/1

build:
	@cabal build --enable-benchmarks all

format:
	@ormolu -i **/*.hs

check:
	@hlint src test
	@ormolu -m check **/*.hs

test: build
	@cabal test
mizchimizchi

StateMonad の使い方を思い出すのに簡単なのを生成させた

簡単な State モナドのコードを書いてユーザーに説明して

module StateMonadExamples (runStateMonadExamples) where

import qualified Control.Monad.State as State

-- State モナドの例
runStateMonadExamples :: IO ()
runStateMonadExamples = do
  putStrLn "State モナドの例"
  putStrLn "================"

  -- 基本的な State モナドの例
  putStrLn "\n1. 基本的なカウンター例:"
  let initialState :: Int = 0
  let (result1, finalState1) = State.runState simpleCounter initialState
  putStrLn $ "  結果: " ++ show result1
  putStrLn $ "  最終状態: " ++ show finalState1

  -- 複数の操作を組み合わせた例
  putStrLn "\n2. 複数の操作を組み合わせた例:"
  let (result2, finalState2) = State.runState combinedOperations 0
  putStrLn $ "  結果: " ++ show result2
  putStrLn $ "  最終状態: " ++ show finalState2

  -- スタックマシンの例
  putStrLn "\n3. スタックマシンの例:"
  let program = [Push 10, Push 5, Add, Push 3, Multiply]
  let (result3, finalStack) = State.runState (executeProgram program) []
  putStrLn $ "  プログラム: " ++ show program
  putStrLn $ "  結果: " ++ show result3
  putStrLn $ "  最終スタック: " ++ show finalStack

simpleCounter :: State.State Int Int
simpleCounter = do
  currentState <- State.get
  State.put (currentState + 1)
  return currentState

combinedOperations :: State.State Int [Int]
combinedOperations = do
  -- 状態を3回更新し、各ステップの状態を記録します
  val1 <- increment
  val2 <- increment
  val3 <- increment
  return [val1, val2, val3]
  where
    increment = do
      current <- State.get
      State.put (current + 1)
      return current

-- | スタックマシンの命令セット
data StackOp = Push Int | Add | Multiply
  deriving (Show)

-- | スタックマシンの実行
-- スタックは [Int] 型で表現します
executeProgram :: [StackOp] -> State.State [Int] (Maybe Int)
executeProgram [] = do
  stack <- State.get
  return $ case stack of
    (top : _) -> Just top
    [] -> Nothing
executeProgram (op : ops) = do
  executeOp op
  executeProgram ops
  where
    executeOp :: StackOp -> State.State [Int] ()
    executeOp (Push n) = do
      stack <- State.get
      State.put (n : stack)
    executeOp Add = do
      stack <- State.get
      case stack of
        (x : y : rest) -> State.put ((y + x) : rest)
        _ -> State.put stack -- スタックに十分な要素がない場合は何もしない
    executeOp Multiply = do
      stack <- State.get
      case stack of
        (x : y : rest) -> State.put ((y * x) : rest)
        _ -> State.put stack -- スタックに十分な要素がない場合は何もしない

mizchimizchi

自分がわかってないのが、この部分

executeProgram [] = do
  stack <- State.get

再帰で executeOp を処理して、最終的に空になり [] でこれが呼ばれるのはわかる。
ただ、 stack <- State.get が関数スコープにない ops の結果の Stateをなぜ解決できているのか


ユーザーの質問は非常に鋭いです。executeProgram []の中でstack <- State.getが呼ばれたとき、なぜ関数スコープにないopsの結果のStateを解決できるのかという点です。

これはHaskellのStateモナドの動作の本質に関わる重要な質問です。実際、stack <- State.getは関数スコープ内の変数ではなく、モナドの文脈(context)から状態を取得しています。

この仕組みを理解するには、Stateモナドの内部実装と、モナドの連鎖(bind操作)がどのように状態を伝播させるかを詳しく説明する必要があります。

Stateモナドは基本的に「状態を引数として受け取り、結果と新しい状態のペアを返す関数」をラップしたものです。State s a型は実質的にs -> (a, s)という関数型です。

executeProgram関数が再帰的に呼び出される過程で、各ステップの状態が次のステップに渡されていきます。最終的にexecuteProgram []が呼ばれたとき、その時点での最新の状態がモナドの文脈に含まれています。State.getはその文脈から現在の状態を取得するのです。

...

executeProgram []の中のstack <- State.getが前の操作の状態を解決できる理由は:

Stateモナドが「状態を受け取り、結果と新しい状態を返す関数」をラップしている
bind操作(>>=)が前の計算の状態を次の計算に渡している
再帰呼び出しの連鎖を通じて、全ての操作による状態変更が蓄積されている
State.getはその時点でのモナドの文脈に含まれる最新の状態を取得している
これがHaskellのStateモナドの強力さであり、関数型プログラミングの美しさの一つです。状態の変更を追跡する責任がプログラマからモナドの実装に移り、より宣言的で読みやすいコードが書けます。


わかった。executeProgram は State.State ... を返しており、do 記法自体が bind 処理を解決しているので State.get はモナドのコンテキストを参照して解決できている、という理解であってる?


はい、その理解は完全に正確です!

executeProgramはState.State [Int] (Maybe Int)型を返しており、do記法自体がbind処理(>>=)を解決しているので、State.getはモナドのコンテキストを参照して状態を解決できています。

重要なポイント
モナドのコンテキスト: Stateモナドは状態をコンテキスト(文脈)として持ち運びます。このコンテキストはbind操作によって関数間で伝播します。

do記法とbind: do記法はbind(>>=)演算子の構文糖衣です。コンパイラは内部的にdo記法をbindの連鎖に変換します。

状態の伝播: executeOp op >>= (_ -> executeProgram ops)という連鎖により、executeOp opの実行後の状態がexecuteProgram opsに渡されます。

State.get: State.getはその時点でのモナドのコンテキストに含まれる状態を取得する操作です。

-- do記法
executeProgram (op : ops) = do
executeOp op
executeProgram ops

-- bind操作に変換
executeProgram (op : ops) = executeOp op >>= (_ -> executeProgram ops)


あいや - aiya000あいや - aiya000

意図がわからなかったので見当違いのことを言っていたら申し訳ないのですが、もしState.getがそもそもどこから[Int]を取ってくるのかの動作原理の話でしたら、まずはReader.askを見てみるといいかもしれません🙆‍♂️
(ReaderはStateのうち読み込みの文脈に相当します。Readerの実際はただの引数渡しを暗黙にしただけのもので、それはReaderの定義type Reader r a = r -> aに現れています。(実際はtypeではなくnewtype))

mizchimizchi

async

module AsyncExamples (runAsyncExamples) where

import Control.Concurrent (threadDelay)
import qualified Control.Concurrent.Async as Async
import Control.Exception (SomeException, catch)
import Data.Time.Clock (diffUTCTime, getCurrentTime)

basicAsyncExample :: IO ()
basicAsyncExample = do
  putStrLn "  非同期タスクを開始します..."
  asyncTask <- Async.async $ do
    putStrLn "  非同期タスクが実行中..."
    threadDelay (2 * 1000000) -- 2秒待機 (マイクロ秒単位)
    putStrLn "  非同期タスクが完了しました"
    return (42 :: Int) -- 結果を返す
  putStrLn "  メインスレッドは他の処理を続行できます"
  threadDelay (1 * 1000000) -- 1秒待機
  result <- Async.wait asyncTask
  putStrLn $ "  非同期タスクの結果: " ++ show result
mizchimizchi

Free Monad

Free モナド版の State を実装して

import Control.Monad.Free (Free (Free, Pure), liftF)

-- 命令セット(コマンド)の定義
data StateCmd s next where
  Get :: (s -> next) -> StateCmd s next
  Put :: s -> next -> StateCmd s next
  Log :: String -> next -> StateCmd s next

-- Functor インスタンスの手動定義
instance Functor (StateCmd s) where
  fmap f (Get g) = Get (f . g)
  fmap f (Put s next) = Put s (f next)
  fmap f (Log msg next) = Log msg (f next)

-- Free モナドの型シノニム
type StateF s a = Free (StateCmd s) a

-- スマートコンストラクタ
get :: StateF s s
get = liftF $ Get id

put :: s -> StateF s ()
put s = liftF $ Put s ()

logMsg :: String -> StateF s ()
logMsg msg = liftF $ Log msg ()

-- StateF プログラムの例
incrementCounter :: StateF Int ()
incrementCounter = do
  current <- get
  logMsg $ "Current value: " ++ show current
  put (current + 1)

-- インタープリター (IO モナドで実行)
interpret :: StateF Int a -> Int -> IO (a, Int)
interpret (Pure a) s = return (a, s)
interpret (Free cmd) s = case cmd of
  Get f -> interpret (f s) s
  Put s' next -> interpret next s'
  Log msg next -> do
    putStrLn msg
    interpret next s

runExample :: IO ()
runExample = do
  (_, finalState) <- interpret program 0
  putStrLn $ "Final state: " ++ show finalState
  where
    program = do
      incrementCounter
      incrementCounter
      incrementCounter

-- | メイン関数
main :: IO ()
main = runExample
mizchimizchi

「Functor を分離する」や liftF の概念を説明して


Haskell の Free モナドにおいて「Functor を分離する」という概念は非常に重要です。これは、計算の「形」(構造)と「中身」(値)を分離するという考え方です。

TypeScript の観点から考えると、これは「コンテナ型」と「その中の値」を分離するようなものです。例えば、TypeScript では:

const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

ここで map メソッドは、配列の「形」(配列構造)を保ちながら、「中身」(各要素)を変換しています。これが Functor の本質です。

Haskell では、Functor は以下のように定義されています:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

f は型コンストラクタで、a や b のような型を受け取って具体的な型を作ります。fmap は、a 型の値を b 型に変換する関数を受け取り、f a 型の値を f b 型に変換します。

Free モナドでは、この Functor の概念を利用して、命令セット(コマンド)の「形」と「次の計算」を分離します。例えば、StateCmd s の Functor インスタンスでは:

instance Functor (StateCmd s) where
  fmap f (Get g) = Get (f . g)
  fmap f (Put s next) = Put s (f next)
  fmap f (Log msg next) = Log msg (f next)

ここで、各コマンドの「形」(Get、Put、Log)は保持しながら、「次の計算」(next や g)に関数 f を適用しています。これにより、コマンドの連鎖を構築できます。

mizchimizchi

Free モナドの内部構造は以下のように定義されています:

data Free f a = Pure a | Free (f (Free f a))

ここで:

Pure a は、純粋な値 a を表します。これは計算の終了を意味します。
Free (f (Free f a)) は、Functor f の中に次の Free モナド計算が含まれていることを表します。これは計算のステップを意味します。
例えば、incrementCounter 関数は内部的には以下のような構造になっています:

incrementCounter =
  Free (Get (\current ->
    Free (Log ("Current value: " ++ show current) (
      Free (Put (current + 1) (
        Pure ()
      ))
    ))
  ))

この構造は、「現在の値を取得し、それをログに出力し、値を1増やす」という一連の操作を表現しています。

Free モナドの強力な点は、このような構造を do 記法を使って簡潔に表現できることです

incrementCounter = do
  current <- get
  logMsg $ "Current value: " ++ show current
  put (current + 1)
mizchimizchi

instance Functor (StateCmd s) where は Rust の impl for のような理解であってる?


はい、その理解は正確です!Haskell の

instance Functor (StateCmd s) where
  fmap f (Get g) = Get (f . g)
  fmap f (Put s next) = Put s (f next)
  fmap f (Log msg next) = Log msg (f next)

は、Rust の

impl<S> Functor for StateCmd<S> {
    fn fmap<A, B>(&self, f: impl Fn(A) -> B) -> Self {
        match self {
            StateCmd::Get(g) => StateCmd::Get(|s| f(g(s))),
            StateCmd::Put(s, next) => StateCmd::Put(*s, f(next)),
            StateCmd::Log(msg, next) => StateCmd::Log(msg.clone(), f(next)),
        }
    }
}

に非常に似ています。

両方とも:

型に対するトレイト/タイプクラスの実装: Haskell では StateCmd s 型に対して Functor タイプクラスを実装し、Rust では StateCmd<S> 型に対して Functor トレイトを実装します。

メソッドの実装: Haskell では fmap 関数を実装し、Rust でも同様に fmap メソッドを実装します。

パターンマッチング: 両方とも、データ型のバリアント(コンストラクタ)ごとに異なる処理を行うためにパターンマッチングを使用します。

型パラメータ: Haskell の s と Rust の S は、どちらも型パラメータとして機能します。

主な違いは構文とセマンティクスの詳細ですが、概念的には非常に似ています。どちらも「この型はこのインターフェース(トレイト/タイプクラス)を実装している」ということを表現しています。

mizchimizchi

EffectMonad

Polysemy を使った State を実装して比較したい

module Main (main) where

import Polysemy (Member, Sem, embedToFinal, runFinal)
import Polysemy.State (State, get, put, runState)
import Polysemy.Trace (Trace, trace, traceToStdout)

-- StateF プログラムの例
incrementCounter :: (Member (State Int) r, Member Trace r) => Sem r ()
incrementCounter = do
  current <- get @Int
  trace $ "Current value: " ++ show current
  put (current + 1)

-- プログラム全体
program :: (Member (State Int) r, Member Trace r) => Sem r Int
program = do
  incrementCounter
  incrementCounter
  incrementCounter
  get @Int -- 最終的な状態を返す

-- | メイン関数
main :: IO ()
main = do
  let state = runState @Int 0 program
  let traceProgram = traceToStdout state
  let ioProgram = embedToFinal @IO traceProgram
  (finalValue, _) <- runFinal ioProgram
  putStrLn $ "Final state: " ++ show finalValue
mizchimizchi

これは Trace と State の EffectMonad という理解だが、Free と違ってライブラリの実装を呼んでるので Functor から自由になってるかどうかが見えない。
Free と どのように違う?


Free モナドでは、エフェクトを表現するデータ型に対して 明示的に Functor インスタンスを定義する必要があります。

data StateCmd s next where
  Get :: (s -> next) -> StateCmd s next
  Put :: s -> next -> StateCmd s next
  Log :: String -> next -> StateCmd s next

-- Functor インスタンスの手動定義が必要
instance Functor (StateCmd s) where
  fmap f (Get g) = Get (f . g)
  fmap f (Put s next) = Put s (f next)
  fmap f (Log msg next) = Log msg (f next)

この Functor インスタンスの定義は、エフェクトの種類が増えるたびに手動で更新する必要があり、ボイラープレートコードが増えていきます。

Polysemy の場合
Polysemy では、エフェクトを表現するデータ型に対して Functor インスタンスを定義する必要がありません。代わりに、GADTs と型レベルリストを使用して、エフェクトを表現します。

Polysemy の内部実装を簡略化すると、以下のようになっています:

data Sem r a where
  Pure :: a -> Sem r a
  Bind :: Sem r a -> (a -> Sem r b) -> Sem r b
  Weave :: Functor f => f (Sem r a) -> Sem r (f a)
  Send :: Union r (Sem r) a -> Sem r a

data Union (r :: [* -> * -> *]) m a where
  Union :: Member t r => t m a -> Union r m a

makeSem は Template Haskell を使用して、エフェクトに対応するスマートコンストラクタ(get、put など)を自動生成します。これにより、ボイラープレートコードが大幅に削減されます。

mizchimizchi

ライブラリそのまま使ってるので違いがわからん

あいや - aiya000あいや - aiya000

いかんせん僕も2年くらいはHaskellを触っていないのでPolysemyを知らないのですが、見たところextensible-effects(freer)の最新実装のようなものと見受けました

おそらくAIはその文脈として、あくまで「Freeとextensible-effectsを比較」している気がします👀

Effect Monad(パラメーターを持つモナド・Graded Monad⋯という概念だったはず)に注目するなら、まずはEffect Monad自体を見た方がいいかも?
https://hackage.haskell.org/package/effect-monad

あいや - aiya000あいや - aiya000

あとはここ周りの質問が何か助けになれば、幸いです🫡


Haskellについて質問です。
extensible-effectsとは、freerのことですか?
そしてfreerとは、FreeモナドとCoyonedaの組み合わせのことですか?
Haskellにおけるextensible-effectsとfreer、そしてFreeモナドとCoyonedaの関係性についてご質問ですね。以下にそれぞれの概念と関係性を説明します。
extensible-effectsとfreer
 * extensible-effectsとfreerは、Haskellにおけるエフェクトシステムを実装するためのライブラリです。
 * どちらも、関数型プログラミングにおけるエフェクト(副作用)を柔軟に扱うための手法を提供します。
 * 両者は目的を同じくしていますが、実装のアプローチが異なります。freerは、FreeモナドとCoyonedaをベースにしているのに対し、extensible-effectsは、より一般的な型クラスと型ファミリーを使用しています。
 * 両者ともエフェクトを扱うライブラリですが、実装方法が異なります。
freerとFreeモナド、Coyoneda
 * freerは、FreeモナドとCoyonedaを組み合わせることで、効率的かつ柔軟なエフェクト処理を実現しています。
 * Freeモナドは、任意の関手(Functor)を持ち上げ、モナドに変換する構造です。これにより、エフェクトを値として扱い、合成や操作を可能にします。
 * Coyonedaは、関手の表現を最適化するための構造です。Freeモナドと組み合わせることで、パフォーマンスを向上させることができます。
 * つまり、freerはFreeモナドとCoyonedaの特性を活かし、より実用的なエフェクトシステムを提供していると言えます。
まとめ
 * extensible-effectsとfreerは、Haskellのエフェクトシステムを扱うためのライブラリであり、目的は同じですが実装が異なります。
 * freerはFreeモナドとCoyonedaを基盤としており、それらの特性を活かす事で効率的かつ柔軟なエフェクト処理を可能にしています。
Haskellのエフェクトシステムは高度な概念であり、理解が難しい部分もあるかと思います。さらに詳しい情報や具体的なコード例については、それぞれのライブラリのドキュメントや関連する論文、記事などを参照することをおすすめします。
mizchimizchi

coyoneda名前だけ見るけど理解してないやつなので、追ってみます

ゆきくらげゆきくらげ

Polysemy はつよつよ Free だと思えば大丈夫だと思います。

まず Free → Freer
Freer は Coyoneda を Free にくっつけたもので、要は Free を作る時の Functor 制約を吹き飛ばせるものです
Coyoneda は任意の data F :: * -> * に対して Coyoneda F :: * -> * を Functor にすることができるんですね これで Freer F = Free (Coyoneda F) と定義することで Free の Functor 制約を満たすことができます

Freer → Extensible Effects
Extensible Effects は Effect の合成ができます。Freer は一つの F に対し Freer F という形でモナドを作れるものなんですが、Effect は複数のエフェクト F, G, H... を組み合わせて Effect (F, G, H...) という形でモナドを作れて、それぞれが並列に合成されるので便利です。さらに一部のエフェクトのみを解釈する操作ができ、例えば runF :: Effect (F, G, H...) → Effect (F, H...) みたいなものが用意できます
(内部的には Freer と Functor の余積との組み合わせ、だと思います)

Extensible Effects → Polysemy
Extensible Effects では新しい Effect を作るのに結構ボイラープレートが必要なんですが、Polysemy では TemplateHaskell (要はマクロ) を使うことで大幅に省略されてます
あと大きな違いは Higher-order Effects をサポートしてることで、すなわち計算を受け取るオペレーション、を定義できます 
例えば local という、区切った範囲内で Read effect の ask の値を変えるオペレーションが使えます
つまり

runLocal $
  runReader "Global" $ do
    x <- ask 
    local "Local" $ do
      y <- ask
      ...
    ...

みたいに書くと x と y の値を変えられるわけです。ここで local もエフェクトなので、runLocal で挙動を変えられます。(例えば "Local" に置き換える普通の挙動や、置き換えず "Global" をそのまま使う挙動を切り替えることができる)