yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

ChatGPTに関数型プログラミングの概念一覧とモナドについて聞いてみた

ChatGPTを使ってよく分かってなかった関数型プログラミングを学んだので学習過程を書いてみます。

まず関数型プログラミング言語の重要な概念をChatGPTに聞く

Q. 関数型プログラミングの重要な概念について教えて

以下ChatGPTの答え

  • モナド (Monad): モナドは副作用の管理や非純粋な操作を抽象化するための概念です。例えば、Maybe モナドや IO モナドなどがあります。
  • カリー化 (Currying): カリー化は、複数の引数を持つ関数を、1つの引数を受け取る関数の連続呼び出しに変換するプロセスです。これにより、部分適用やカリー化された関数を合成するなど、関数の組み合わせが容易になります。
  • 高階関数 (Higher-order Functions): 高階関数は、関数を引数として受け取ったり、関数を戻り値として返したりする関数のことを指します。関数を値として扱うことで、抽象的な処理を行ったり、再利用可能なコンポーネントを作成したりできます。
  • パターンマッチング (Pattern Matching): パターンマッチングは、データの構造や内容に基づいて異なる処理を行うための機能です。条件分岐の複雑さを削減し、柔軟な処理を実現します。
  • リスト操作 (List Manipulation): リストや他のコレクションを操作する関数(例: map、filter、reduce)が関数型プログラミングで重要な役割を果たします。これにより、反復処理を抽象化し、コードを簡潔かつ表現力豊かにします。
  • 不変性 (Immutability): 不変性は、データの変更を許さない性質を指します。関数型プログラミングでは、不変なデータ構造を使用することで、副作用を減少させ、プログラムの信頼性を向上させます。
  • ラムダ式 (Lambda Expressions): ラムダ式は、無名の関数を定義するための方法です。関数をデータとして扱う関数型プログラミングにおいて、ラムダ式は非常に重要な機能です。
  • 再帰 (Recursion): 再帰は、関数が自身を呼び出すことで問題を解決する手法です。関数型言語ではループよりも再帰が一般的であり、データの構造に合わせた再帰的な処理が行われます。
  • コンビネータ (Combinators): コンビネータは、関数を組み合わせて新しい関数を生成する抽象化です。例えば、関数合成や部分適用はコンビネータの一例です。

上記のうちよくわからないキーワードを掘り下げてみる 上記の個人的な理解度を書くと

ちょっとわかる

再帰→得意じゃないけどわかる

カリー化、高階関数→「関数の引数に関数を指定して引数を減らす」というテクニックは純粋な関数型言語でなくとも使われていて実務でも触れたことがあるのでまあ多分大丈夫

リスト操作→JsとかRubyに入ってるmapとかeachみたいなもんだと思っている

ラムダ式Rubyでラムダやブロックでカプセル化したコードは散々書いてるのでよしとする

ふわっとしかわからない

モナドについて教えて

以下ChatGPTの答え

モナドは、関数型プログラミングモナド理論において重要な概念です。モナドは、副作用や非純粋な操作を扱う際にプログラムの構造と制御を保持するための仕組みです。特に、純粋な関数型プログラミングの中で副作用を制御するために使用されます。

モナドの基本的なアイデアは、以下の3つの要素から成り立っています:

  • 型コンストラクタ(Type Constructor): モナドは、特定の型に対するコンテキストを提供するために使われます。このコンテキストは、値を包んで新しい型を生成するための型コンストラクタです。例えば、リストやオプション(Maybe)などが型コンストラクタの例です。
  • ユニット関数(Unit Function): ユニット関数は、値をモナド内に格納するための操作です。通常、これは値をモナドのコンテキストに入れるための関数です。例えば、リストモナドでは単一の要素からなるリストを生成する操作です。
  • バインド操作(Bind Operation): バインド操作は、モナド内の値に対して関数を適用して新しいモナドを生成する操作です。この操作により、モナドの中の値を取り出し、それに関数を適用し、新しいモナドに入れることができます。これにより、関数を副作用の制御やコンテキストの管理に使用できます。 モナドは、一般的にモナド則と呼ばれる法則を満たす必要があります。これにより、モナドの振る舞いが一貫していて、予測可能であることが保証されます。

モナドは、例えばエラーハンドリング、状態の管理、非同期処理など、さまざまな場面で活用されます。HaskellScalaなどの関数型プログラミング言語では、モナドを使ってこれらの複雑な操作をより安全かつ分かりやすい方法で実現することができます。

これだけだとよくわからないのでHaskellでサンプルコード書いてもらう

Q. Haskellのサンプルコードを使ってモナドを解説して

以下ChatGPTの答え

もちろんです!Haskellのサンプルコードを使って、モナドを具体的に解説してみましょう。以下は、Maybeモナドを使用したエラーハンドリングの例です。

-- Maybeモナドを使った割り算の関数
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)

-- 2つの数を安全に割り算し、結果を表示する関数
divideAndShow :: Double -> Double -> String
divideAndShow a b =
    case safeDivide a b of
        Nothing -> "Error: Division by zero!"
        Just result -> "Result: " ++ show result

main :: IO ()
main = do
    putStrLn $ divideAndShow 10 2
    putStrLn $ divideAndShow 5 

このコードでは、safeDivide関数がMaybeモナドを使用しています。この関数は、割る数が0でない場合には割り算の結果をJustで包み、割る数が0の場合にはエラーをNothingで表現します。divideAndShow関数では、safeDivideを使って割り算の結果を取得し、それに応じてエラーメッセージか結果を表示します。

この例では、Maybeモナドを使用してエラーハンドリングを行っています。モナドを使うことで、副作用やエラー処理をよりコンパクトに、かつ安全に記述することができます。関数型プログラミングでは、このようにモナドを活用してプログラムの構造を保ちつつ複雑な操作を扱うことが一般的です。

Maybeモナドだけだと物足りないかな

Q. Maybeモナド以外にどんなモナドがありますか?

以下ChatGPTの答え

モナドは、さまざまなプログラミング言語やコンテキストで使用される概念であり、エラーハンドリングや非同期処理などの複雑な操作を扱う際に有用です。以下にいくつかの代表的なモナドの例を挙げてみましょう:

  • リストモナド(List Monad): リストモナドは、複数の値を扱う際に使用されます。リスト内の各値に関数を適用し、新しいリストを生成します。このモナドを使うことで、リスト内の各要素に順次関数を適用し、新しいリストを生成する処理を簡潔に記述できます。
  • IOモナド(IO Monad): IOモナドは、副作用(入出力操作など)を抽象化するために使用されます。純粋な関数型プログラミング言語では、副作用を持つ操作は外部の状態を変更する可能性がありますが、IOモナドを使うことでこれらの副作用を制御できます。
  • Stateモナド(State Monad): Stateモナドは、状態を扱う際に使用されます。関数内で状態を保持し、それを更新しながら操作を行うことができます。このモナドを使用すると、状態を明示的に引数として渡す必要がなくなり、コードがより読みやすくなります。
  • Eitherモナド: Eitherモナドは、成功か失敗のどちらかを表す場合に使用されます。通常、成功の場合は値を持ち、失敗の場合はエラー情報などを持ちます。エラーハンドリングに便利です。
  • Writerモナド: Writerモナドは、ログを取りながら計算を行う場合に使用されます。計算過程でのログを追加するためのメカニズムを提供します。
  • Readerモナド: Readerモナドは、環境設定を共有しながら計算を行う場合に使用されます。特定の設定を関数内で共有することができます。 これらは一部のモナドの例であり、他にもさまざまなモナドが存在します。モナド関数型プログラミングの中で重要な役割を果たす概念であり、コードの再利用性や保守性を高めるために利用されます。

やっぱりサンプルコードが欲しい 聞いたらちゃんと出してくれた。すごい。

Q. 上記のモナドHaskellのサンプルコードで解説して

以下ChatGPTの答え

もちろんです!それぞれのモナドHaskellのサンプルコードを使って解説してみましょう。

リストモナド(List Monad

-- リストモナドを使った値の合成
listMonadExample :: [Int]
listMonadExample = do
    x <- [1, 2, 3]
    y <- [4, 5, 6]
    return (x + y)

main :: IO ()
main = print listMonadExample

このコードでは、do構文を使用して2つのリストから値を選び、それぞれの値を足して新しいリストを生成しています。

IOモナド(IO Monad

-- IOモナドを使った副作用の操作
ioMonadExample :: IO ()
ioMonadExample = do
    putStrLn "Hello, what's your name?"
    name <- getLine
    putStrLn $ "Nice to meet you, " ++ name ++ "!"

main :: IO ()
main = ioMonadExample

このコードでは、putStrLnやgetLineなどの副作用を持つ操作をdo構文を使って連結しています。これにより、副作用を制御しながらプログラムを実行できます。

Stateモナド

import Control.Monad.State

-- Stateモナドを使った状態の操作
stateMonadExample :: State Int Int
stateMonadExample = do
    x <- get
    put (x + 1)
    return x

main :: IO ()
main = do
    let (result, newState) = runState stateMonadExample 5
    putStrLn $ "Result: " ++ show result
    putStrLn $ "New State: " ++ show newState
このコードでは、Stateモナドを使って状態を操作しています。getで現在の状態を取得し、putで新しい状態を設定します。

Eitherモナド:

-- Eitherモナドを使ったエラーハンドリング
eitherMonadExample :: Either String Int
eitherMonadExample = do
    x <- Right 10
    y <- Left "Error: Something went wrong"
    return (x + y)

main :: IO ()
main = case eitherMonadExample of
    Left err -> putStrLn err
    Right result -> print result
このコードでは、Eitherモナドを使用してエラーハンドリングを行っています。最初のエラーが返されるため、最終的な結果もエラーになります。

Writerモナド:

import Control.Monad.Writer

-- Writerモナドを使ったログの記録
writerMonadExample :: Writer [String] Int
writerMonadExample = do
    tell ["Starting calculation"]
    x <- return 10
    tell ["Value is " ++ show x]
    y <- return 20
    tell ["Value is " ++ show y]
    return (x + y)

main :: IO ()
main = do
    let (result, log) = runWriter writerMonadExample
    putStrLn "Log:"
    mapM_ putStrLn log
    putStrLn $ "Result: " ++ show result

このコードでは、Writerモナドを使ってログを記録しています。tellを使用してログを追加し、最終的な計算結果とログを取得します。

Readerモナド:

import Control.Monad.Reader

-- Readerモナドを使った環境の共有
readerMonadExample :: Reader Int Int
readerMonadExample = do
    x <- ask
    return (x * 2)

main :: IO ()
main = do
    let result = runReader readerMonadExample 5
    putStrLn $ "Result: " ++ show result

このコードでは、Readerモナドを使用して環境を共有しています。askで環境を取得し、それを使って計算を行います。

これらのサンプルコードを通じて、異なるモナドの使用例とその特徴を理解できるかと思います。

Haskell以外にモナド使ってる言語あるのかな?

Haskellやっぱ読み慣れなし他の言語でサンプルコード頼めんかな。と思って一応聞いてみる。RustとSwiftはちょっと触ったがそんな詳しくないな。。

Q. Haskell以外にモナドを多用するプログラミング言語はある?

以下ChatGPTの答え

はい、Haskell以外にもモナドを多用するプログラミング言語があります。特に関数型プログラミング言語や影響を受けた言語では、モナドに類似する概念やパターンを使用しています。以下にいくつかの例を挙げてみましょう。

RubyとかGoで説明させるとどうなるだろうか 邪道だけどイメージ掴むには良いのでは?

Q. RubyとGoでモナドを解説できる?

以下ChatGptの答え

モナド自体は関数型プログラミングの概念であり、特にHaskellなどの言語で強調されるものです。ただし、RubyやGoなどの他のプログラミング言語でも、モナドと同様のアイデアを一部採用することは可能ですが、直接的なモナドの導入やモナド則の強制は難しい場合があります。これは、モナドHaskellなどの静的型付けを強調する言語の中で特に有用であるためです。

以下に、RubyとGoでモナドに類似するアイデアを導入する方法を簡単に説明しますが、完全なモナドの実装や特徴を再現することは難しいかもしれません。

  • Ruby: Rubyは動的型付けの言語であり、型の厳密さがHaskellよりも低いため、純粋なモナドの導入は難しいかもしれません。ただし、Rubyではブロックやプロキシオブジェクトなどの機能を使って、モナドに類似する操作を行うことができます。

例として、Maybeモナドに似た振る舞いを持つRubyのコードを示します

class Maybe
  def initialize(value)
    @value = value
  end

  def bind(&block)
    if @value.nil?
      Maybe.new(nil)
    else
      block.call(@value)
    end
  end

  def self.unit(value)
    Maybe.new(value)
  end
end

result = Maybe.unit(10)
  .bind { |x| Maybe.unit(x / 2) }
  .bind { |x| Maybe.unit(x + 3) }

puts result.inspect

この例では、Maybeクラスを定義してブロックとbindメソッドを使って、値の操作と合成を行っています。

Go

Goも静的型付け言語であり、モナドの直接的な導入は難しいですが、関数の組み合わせやインターフェースを利用して、モナドに類似した構造を模倣することができます。

以下に、GoでMaybeモナドに類似したコードの例を示します:

package main

import (
    "fmt"
)

type Maybe struct {
    value interface{}
}

func (m Maybe) Bind(f func(interface{}) Maybe) Maybe {
    if m.value == nil {
        return Maybe{nil}
    }
    return f(m.value)
}

func Unit(value interface{}) Maybe {
    return Maybe{value}
}

func main() {
    result := Unit(10).
        Bind(func(x interface{}) Maybe {
            return Unit(x.(int) / 2)
        }).
        Bind(func(x interface{}) Maybe {
            return Unit(x.(int) + 3)
        })

    fmt.Println(result)
}

純粋なモナドと同等のものではないかもしれないけど理解深まった気はする 純粋な関数型言語では上記の例のようなオブジェクト指向のクラスやインスタンスの概念は存在しないけどUnit関数でモナド内に値を格納→Bind関数でモナド内の値に関数を適用して新しいモナドに値を格納する。という処理の内容は伝わる。

なんでこんなことするんだっけ?と思ったけど、引数を複数持たせるのではなく関数を再起的に処理していくための機能ということだった。

読みやすいかは開発者それぞれの経験次第だと思うけどある種の美しさはわからんでもない。

終わり

コンビネータ、普遍性とか他にも関数型言語についてChatGPTで深掘りしたいけど長くなったので一旦区切ります。