0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Golang で Monad 入門

Last updated at Posted at 2024-11-26

Monad とは

モナド(Monad)とは、関数型言語でよく出てくる、プログラムを構造化するための汎用的な抽象概念。
ちなみにモナドの概念や用語は、数学の圏論からきている。(ここでは深く掘り下げない)

解説

ベースコード

例えば、以下のような関数があるとする。

func f(x int) int {
	return x * x
}

func g(x int) int {
	return x + 1
}

ここで、 (x^2)+1 を計算したい時、 (f∘g)(x) のような合成関数が適用できる。
例えば、 x = 2 の場合、以下のような結果が得られる。

func Example() {
	x := 2
	result := g(f(x))
	println(result)
	// Output: 5
}

課題1

では、これらの関数にログを追加したい、となった場合はどうすればよいだろうか?
入力として2を渡し、結果として5を受け取ったが、2から5に至る過程を確認したい。

ということで、各関数にてログ付きの結果を返すように変更してみた。
f() , g() ではわかりずらいので関数名も変更した)

type NumberWithLogs struct {
	Result int
	Logs   []string
}

func Square(x int) NumberWithLogs {
	return NumberWithLogs{
		Result: x * x,
		Logs:   []string{fmt.Sprintf("Squared %d to get %d.", x, x*x)},
	}
}

func AddOne(x NumberWithLogs) NumberWithLogs {
	return NumberWithLogs{
		Result: x.Result + 1,
		Logs:   append(x.Logs, fmt.Sprintf("Added 1 to %d to get %d.", x.Result, x.Result+1)),
	}
}

これで、見た目上は問題ないように見える。

func main() {
	x := 2
	result := AddOne(Square(x))
	fmt.Printf("%+v\n", result)
	// Output: {Result:5 Logs:[Squared 2 to get 4. Added 1 to 4 to get 5.]}
}

一方で、 (x^2)^2 や、 x+1+1 を求めたい場合、以下のような関数呼び出しはコンパイルエラーによって機能しない。

func Example() {
	_ = Square(Square(x)) // cannot use Square(x) (value of type NumberWithLogs) as int value in argument to Square
	_ = AddOne(AddOne(x)) // cannot use x (variable of type int) as NumberWithLogs value in argument to AddOne
}

この問題はwrap関数の導入で解決ができる。

func WrapWithLogs(x int) NumberWithLogs {
	return NumberWithLogs{
		Result: x,
		Logs:   []string{},
	}
}

wrap関数を用いて、前述のエラーを修正してみる。

type NumberWithLogs struct {
	Result int
	Logs   []string
}

func WrapWithLogs(x int) NumberWithLogs {
	return NumberWithLogs{
		Result: x,
		Logs:   []string{},
	}
}

func Square(x NumberWithLogs) NumberWithLogs {
	return NumberWithLogs{
		Result: x.Result * x.Result,
		Logs:   append(x.Logs, fmt.Sprintf("Squared %d to get %d.", x.Result, x.Result*x.Result)),
	}
}

func AddOne(x NumberWithLogs) NumberWithLogs {
	return NumberWithLogs{
		Result: x.Result + 1,
		Logs:   append(x.Logs, fmt.Sprintf("Added 1 to %d to get %d.", x.Result, x.Result+1)),
	}
}

func Example() {
	x := 2
	// `WrapWithLogs()`関数を用いる
	_ = AddOne(Square(WrapWithLogs(x)))
	_ = Square(Square(WrapWithLogs(x)))
	_ = AddOne(AddOne(WrapWithLogs(x)))
}

無事、(x^2)^2 や、x+1+1 なども計算可能になった。

課題2

しかし、これでは各関数内で一々 NumberWithLogs.Result を参照したり、returnの際にログ文字列のappend処理を書かなければならなくて非効率的。

ということで、ログを追加しつつ任意の関数を呼び出すための RunWithLogs() 関数を追加してみる。

func RunWithLogs(
	input NumberWithLogs,
	transform func(int) NumberWithLogs,
) NumberWithLogs {
	newNumberWithLogs := transform(input.Result)
	return NumberWithLogs{
		Result: newNumberWithLogs.Result,
		Logs:   append(input.Logs, newNumberWithLogs.Logs...),
	}
}

これにより、引数 xNumberWithLogs.Result の参照と、ログ文字列のappend処理を RunWithLogs() に委譲することができるようになった。

type NumberWithLogs struct {
	Result int
	Logs   []string
}

func WrapWithLogs(x int) NumberWithLogs {
	return NumberWithLogs{
		Result: x,
		Logs:   []string{},
	}
}

func RunWithLogs(
	input NumberWithLogs,
	transform func(int) NumberWithLogs,
) NumberWithLogs {
	newNumberWithLogs := transform(input.Result)
	return NumberWithLogs{
		Result: newNumberWithLogs.Result,
		Logs:   append(input.Logs, newNumberWithLogs.Logs...),
	}
}

func Square(x int) NumberWithLogs {
	return NumberWithLogs{
		Result: x * x,
		Logs:   []string{fmt.Sprintf("Squared %d to get %d.", x, x*x)},
	}
}

func AddOne(x int) NumberWithLogs {
	return NumberWithLogs{
		Result: x + 1,
		Logs:   []string{fmt.Sprintf("Added 1 to %d to get %d.", x, x+1)},
	}
}

これで、xの変換処理を任意の順序で組み合わせることが可能になったとともに、「ログの連結」という副作用を隠蔽することができた。
(関数型言語じゃないから呼び出しがわかりずらいのはユルシテ...)

func Example() {
	x := 2
	result := RunWithLogs(RunWithLogs(WrapWithLogs(x), Square), Square)
	fmt.Printf("%+v\n", result)
	// Output: {Result:16 Logs:[Squared 2 to get 4. Squared 4 to get 16.]}
}

こうなると、ログの連結処理などの追加の手間を必要とせず、AddTwo()MultiplyByThree() などの新しい関数を追加することもできる。

結局、モナドとは?

前述したコード全体がまさにモナドで、「副作用を型で表現し、関数の参照透過性を担保する」ためのデザインパターンという解釈で差支えないと思う。(厳密には違うけど、概要を理解するための解釈としては十分)

特徴として、全てのモナドは以下の3つのコンポーネントをもつ。

  • Wrapper Type: NumberWithLogs
    • モナドの型
    • 何かの値を wrap するもの
  • Wrap Function: WrapWithLogs()
    • 通常の値を取る関数
    • コンストラクタのような役割
    • 広義では "return", "pure", "unit" と表現される
  • Run Function: RunWithLogs()
    • Wrapper Typeを受け取る関数と、Unwrapされた型を受け入れて、ラッパー型を返す変換関数
    • 広義では "bind", "flatmap", ">>=" と表現される

別の例① Maybe モナド

代表的な別のモナドとして、Maybe モナドがある。 (別名: Option モナド)
これは、値が存在しない可能性を表すものとして使われる。

Wrapper Type

type Maybe[T any] struct {
	v     T
	valid bool
}

func (p Maybe[T]) Value() T { return p.value }

func (p Maybe[T]) IsNone() bool { return !p.valid }

Wrap Function

func Some[T any](v T) Maybe[T] {
	return Maybe[T]{value: v, valid: true}
}

func None[T any]() Maybe[T] {
	return Maybe[T]{valid: false}
}

Run Function

func Run[T, R any](
	input Maybe[T],
	transform func(T) Maybe[R],
) Maybe[R] {
	if input.IsNone() {
		return None[R]()
	}
	return transform(input.value)
}

Sample1

1. モナドを使用していない例

値の存在チェックを毎回行う必要がある

func GetPetNickName() *string {
	user := getCurrentUser()
	if user == nil {
		return nil
	}

	pet := getPet(user)
	if pet == nil {
		return nil
	}

	return getPetNickName(pet)
}

// prototypes
func getCurrentUser() *User
func getPet(*User) *Pet
func getPetNickName(*Pet) *string

2. Maybe モナドを使用した例

Maybe モナドが裏側で値の存在チェックをしてくれる。

func GetPetNickName() Maybe[string] {
	user := getCurrentUser()
	pet := Run(user, getPet)
	return Run(pet, getPetNickName)
}

// prototypes
func getCurrentUser() Maybe[User]
func getPet(User) Maybe[Pet]
func getPetNickName(Pet) Maybe[string]

ちなみに、パイプ演算子がある言語では以下のような呼び出し方ができる。
(Go でパイプ演算子あったら、という想像のコード)

func GetPetNickName() Maybe[string] {
	user := getCurrentUser()
    
	return Maybe do
		Some(user)
		|> getPet
		|> getPetNickName
	end
}

Sample2

シンプルに構造体のオプショナルなフィールドの表現としても活用できる

type User struct {
	Name     string
	NickName Maybe[string]
	Age      uint8
}

別の例② Either モナド

エラーハンドリングの手法として Either モナドというものがある。
RustではResult型とも呼ばれているが、ここでは由緒正しい(?) 関数型言語であるHaskell の例に倣って Either<L, R> と表現する。
Either<L, R> は失敗を表すクラス Left<L> と成功を表すクラス Right<R> のユニオンとして実装されており、Monad としては Right の値を優先して使用するようになっている。
こうすることで前述のMaybeモナドと同様、Either の値をどんどん合成していき、最終的にひとつの計算結果を得ることができる。

Wrapper Type

Golangには元々error型があるので、Right を[T any]、Left はerror固定とした

type Either[T any] struct {
	value T
	err   error
}

Wrap Function

// Right 成功した値を持つEitherを作成
func Right[T any](value T) Either[T] {
	return Either[T]{value: value, err: nil}
}

// Left エラーを持つEitherを作成
func Left[T any](err error) Either[T] {
	return Either[T]{err: err}
}

Run Function

func Run[T, R any](
	input Either[T],
	transform func(T) Either[R],
) Either[R] {
	if input.err != nil {
		return Either[R]{err: input.err}
	}
	return transform(input.value)
}

Sample

1. Either モナドを使用しない例

こちらは golang の一般的な手法。
毎回エラーチェックをする必要がある。

func GetPetNickName() (string, error) {
	user, err := getCurrentUser()
	if err != nil {
		return "", err
	}

	pet, err := getPet(user)
	if err != nil {
		return "", err
	}

	return getPetNickName(pet)
}

// prototypes
func getCurrentUser() (User, error)
func getPet(User) (Pet, error)
func getPetNickName(Pet) (string, error)

2. Either モナドを使用した例

Either モナドが裏側でエラーハンドリングをしてくれる。

func GetPetNickName() Either[string] {
	user := getCurrentUser()
	pet := Run(user, getPet)
	return Run(pet, getPetNickName)
}

// prototypes
func getCurrentUser() Either[User]
func getPet(User) Either[Pet]
func getPetNickName(Pet) Either[string]

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?