Goプログラマ向けのモナド

モナドは、関数合成を構成し、それに関連する退屈な均一性を取り除くために使用されます。 Goでの7年間のプログラミングの後if err != nilがルーチンになったif err != nilに繰り返す必要があります。 この行を書くたびに、優れたツールを備えた読みやすい言語のGophersに感謝しますが、同時に、バートシンプソンに罰せられたと感じたので、私はのろいました。



この気持ちは多くの人共有されていると思うが、


 if err != nil { log.Printf("This should still be interesting to a Go programmer " + "considering using a functional language, despite %v.", err) } 

モナドは、エラー処理を非表示にするためだけでなく、リストの包含、一貫性、およびその他のタスクのためにも必要です。


これを読まないでください


Erik MeijerはEdxの関数型プログラミングコースの紹介で、モナドについてはすでに多くのことを述べているので、モナドについてはもう書かないように求めています。


この記事を読むことに加えて、カテゴリー理論に関するBartosz Milewskiの一連のビデオを見ることをお勧めします 。これは、私が今まで出会ったモナドのより良い説明で終わります。


これ以上読むな!


ファンクター


まあ(ため息)...覚えておいてください:私はあなたに警告しました。
モナドの前に、ファンクターに対処する必要があります。 ファンクタはモナドのスーパークラスです。つまり、すべてのモナドはファンクタです。 ファンクタを使用してモナドの本質をさらに説明しますので、このセクションをスキップしないでください。


ファンクターは、1つのタイプのエレメントを含むコンテナーと見なすことができます。


例:



Goを話さないが他の言語を話すプログラマ:Goには代数的なデータ型やユニオン型はありません。 これは、関数によって値またはエラーを返す代わりに、値エラーがここに返され、それらの1つが通常nilであることを意味します。 時々、お互いを混同しようとするために、慣例を破り、両方ともnilではなく値とエラーを返します。 または、楽しんでください。


Goで結合された型を取得する最も一般的な方法は、インターフェイス(抽象クラ​​ス)とインターフェイスの型に型スイッチを設定することです。


コンテナーがファンクターと見なされない別の基準は、このタイプのコンテナーにfmap関数を実装する必要があることです。 fmap関数は、コンテナまたは構造を変更せずに、コンテナ内の各要素に関数を適用します。


 func fmap(f func(A) B, aContainerOfA Container<A>) Container<B> 

map関数をスライスとして使用することは、Hadoop、Python、Ruby、および他のほぼすべての言語でmapreduceに出くわすかもしれない古典的な例です。


 func fmap(f func(A) B, as []A) []B { bs := make([]b, len(as)) for i, a := range as { bs[i] = f(a) } return bs } 

ツリーにfmapを実装することもできます。


 func fmap(f func(A) B, atree Node<A>) Node<B> { btree := Node<B>{ Value: f(atree.Value), Children: make([]Node<B>, len(atree.Children)), } for i, c := range atree.Children { btree.Children[i] = fmap(f, c) } return btree } 

または、チャネルの場合:


 func fmap(f func(A) B, in <-chan A) <-chan B { out := make(chan B, cap(in)) go func() { for a := range in { b := f(a) out <- b } close(out) }() return out } 

または、ポインターの場合:


 func fmap(f func(A) B, a *A) *B { if a == nil { return nil } b := f(*a) return &b } 

または機能の場合:


 func fmap(f func(A) B, g func(C) A) func(C) B { return func(c C) B { a := g(c) return f(a) } } 

または、エラーを返す関数の場合:


 func fmap(f func(A) B, g func() (*A, error)) func() (*B, error) { return func() (*B, error) { a, err := g() if err != nil { return nil, err } b := f(*a) return &b, nil } } 

対応するfmap実装を持つこれらのコンテナはすべてファンクターです。


機能構成


これで、ファンクターがコンテナの抽象名であり、コンテナ内の要素に関数を適用できることがわかりました。 次に、記事の本質、つまりモナドの抽象的な概念に目を向けます。


モナドは単なる「装飾された」タイプです。 うーん、はっきりしていない、抽象的すぎると思う。 これは、モナドの本質に関するすべての説明の典型的な問題です。 副作用とは何かを説明しようとしているようなものです。説明が一般的すぎます。 それでは、モナドの抽象性の理由をよりよく理解しましょう。 その理由は、これらの装飾された型が返す関数を構成するためです。


装飾された型なしで、関数の通常のレイアウトから始めましょう。 この例では、関数fgを構成し、 f期待する入力を受け取り、 gから出力を返すfを返します。


 func compose(f func(A) B, g func(B) C) func(A) C { return func(a A) C { b := f(a) c := g(b) return c } } 

明らかに、これは、結果の型fが入力型g一致する場合にのみ機能します。


別のバージョン:エラーを返すリンク関数。


 func compose( f func(*A) (*B, error), g func(*B) (*C, error), ) func(*A) (*C, error) { return func(a *A) (*C, error) { b, err := f(a) if err != nil { return nil, err } c, err := g(b) return c, err } } 

次に、このエラーを装飾Mの形で抽象化して、残っているものを確認してください。


 func compose(f func(A) M<B>, g func(B) M<C>) func(A) M<C> { return func(a A) M<C> { mb := f(a) // ... return mc } } 

入力パラメーターとしてAを受け取る関数を返す必要があるため、戻り関数を宣言することから始めましょう。 Aので、 fを呼び出してタイプM<b> mb値を取得できますが、次はどうでしょうか?


目標が抽象的すぎることが判明したため、目標を達成できませんでした。 私たちはmbを持っていると言いたいのですが、どうすればいいのでしょうか?


これが間違いであることを知ったとき、私たちはそれをチェックすることができましたが、それはあまりにも抽象的であるため、今ではできません。


しかし...装飾Mもファンクターであることがわかっている場合は、 fmapを適用できます。


 type fmap = func(func(A) B, M<A>) M<B> 

fmapを適用したいg関数はCような単純な型を返さず、 M<C>返します。 幸いなことに、 fmapこれは問題ではありませんが、型のシグネチャは変わります。


 type fmap = func(func(B) M<C>, M<B>) M<M<C>> 

これで、タイプM<M<C>> mmc値が得られました。


 func compose(f func(A) M<B>, g func(B) M<C>) func(A) M<C> { return func(a A) M<C> { mb := f(a) mmc := fmap(g, mb) // ... return mc } } 

M<M<C>>からM<C>ます。


このためには、装飾M単なるファンクターであるだけでなく、別のプロパティも持っている必要があります。 このプロパティは、各ファンクターに対してfmapが定義されているため、各モナドに対して定義されているjoin関数です。


 type join = func(M<M<C>>) M<C> 

次のように書くことができます:


 func compose(f func(A) M<B>, g func(B) M<C>) func(A) M<C> { return func(a A) M<C> { mb := f(a) mmc := fmap(g, mb) mc := join(mmc) return mc } } 

つまり、装飾中にfmapjoin定義されている場合、装飾された型を返す2つの関数を組み合わせることができます。 つまり、これらの2つの関数は、型がモナドになるように定義する必要があります。


参加する


モナドはファンクターなので、再びモナドを定義する必要はありません。 joinを定義joinだけです。


 type join = func(M<M<C>>) M<C> 

join定義します:



リスト式


開始する最も簡単な方法は、スライスにjoinを適用joinことです。 この関数は、すべてのスライスを単純に連結します。


 func join(ss [][]T) []T { s := []T{} for i := range ss { s = append(s, ss[i]...) } return s } 

もう一度join必要join理由を見てみましょうが、今回はスライスに焦点を当てます。 それらのcompose関数は次のとおりです。


 func compose(f func(A) []B, g func(B) []C) func(A) []C { return func(a A) []C { bs := f(a) css := fmap(g, bs) cs := join(css) return cs } } 

afに渡すとaタイプ[]B bsを取得し[]B


これで、 gを使用してfmap[]B適用でき、 [][]Cではなく[]C [][]Cような値が得られます。


 func fmap(g func(B) []C, bs []B) [][]C { css := make([][]C, len(bs)) for i, b := range bs { css[i] = g(b) } return css } 

それが私たちがjoin必要joincssからcs 、または[][]Cから[]C [][]C[]C


より具体的な例を見てみましょう。


型を置き換える場合:



次に、関数は次のようになります。


 func compose(f func(int) []int64, g func(int64) []string) func(int) []string func fmap(g func(int64) []string, bs []int64) [][]string func join(css [][]string) []string 

これで、例でそれらを使用できます。


 func upto(n int) []int64 { nums := make([]int64, n) for i := range nums { nums[i] = int64(i+1) } return nums } func pair(x int64) []string { return []string{strconv.FormatInt(x, 10), strconv.FormatInt(-1*x, 10)} } c := compose(upto, pair) c(3) // "1","-1","2","-2","3","-3" 

これは、最初のモナドのスライスです。


奇妙なことに、これはリスト式がHaskellでどのように機能するかです:


 [ y | x <- [1..3], y <- [show x, show (-1 * x)] ] 

しかし、Pythonの例を使用してそれらを認識できます。


 def pair (x): return [str(x), str(-1*x)] [y for x in range(1,4) for y in pair(x) ] 

単項エラー処理


値とエラーを返す関数のjoinを定義joinこともできます。 これを行うには、Goのいくつかの特異性のために、最初にfmap関数に戻る必要があります。


 type fmap = func(f func(B) C, g func(A) (B, error)) func(A) (C, error) 

ビルド関数が関数f fmapを呼び出すことを知っていますが、これもエラーを返します。 その結果、 fmapシグネチャは次のようになります。


 type fmap = func( f func(B) (C, error), g func(A) (B, error), ) func(A) ((C, error), error) 

残念ながら、Goのタプルは第1レベルのオブジェクトではないため、次のように書くことはできません。


 ((C, error), error) 

この困難を回避する方法はいくつかあります。 タプルを返す関数は第1レベルのオブジェクトであるため、私はこの関数を好みます。


 (func() (C, error), error) 

ここで、トリックを使用して、値とエラーを返す関数のfmapを定義できます。


 func fmap( f func(B) (C, error), g func(A) (B, error), ) func(A) (func() (C, error), error) { return func(a A) (func() (C, error), error) { b, err := g(a) if err != nil { return nil, err } c, err := f(b) return func() (C, error) { return c, err }, nil } } 

これにより、主なポイントに戻ります: (func() (C, error), error)適用されるjoin関数。 解決策は簡単で、エラーチェックの1つを実行します。


 func join(f func() (C, error), err error) (C, error) { if err != nil { return nil, err } return f() } 

すでにjoinfmap定義しているため、 compose関数を使用できます。


 func unmarshal(data []byte) (s string, err error) { err = json.Unmarshal(data, &s) return } getnum := compose( unmarshal, strconv.Atoi, ) getnum(`"1"`) // 1, nil 

その結果、モナドはjoin関数を使用してバックグラウンドでこれを行うため、より少ないエラーチェックを実行する必要があります。


バートシンプソンのように感じる別の例を次に示します。


 func upgradeUser(endpoint, username string) error { getEndpoint := fmt.Sprintf("%s/oldusers/%s", endpoint, username) postEndpoint := fmt.Sprintf("%s/newusers/%s", endpoint, username) req, err := http.Get(genEndpoint) if err != nil { return err } data, err := ioutil.ReadAll(req.Body) if err != nil { return err } olduser, err := user.NewFromJson(data) if err != nil { return err } newuser, err := user.NewUserFromUser(olduser), if err != nil { return err } buf, err := json.Marshal(newuser) if err != nil { return err } _, err = http.Post( postEndpoint, "application/json", bytes.NewBuffer(buf), ) return err } 

技術的には、 composeは2つ以上の関数をパラメーターとして取ることができます。 したがって、上記のすべての関数を1回の呼び出しで収集し、例を書き換えます。


 func upgradeUser(endpoint, username string) error { getEndpoint := fmt.Sprintf("%s/oldusers/%s", endpoint, username) postEndpoint := fmt.Sprintf("%s/newusers/%s", endpoint, username) _, err := compose( http.Get, func(req *http.Response) ([]byte, error) { return ioutil.ReadAll(req.Body) }, newUserFromJson, newUserFromUser, json.Marshal, func(buf []byte) (*http.Response, error) { return http.Post( postEndpoint, "application/json", bytes.NewBuffer(buf), ) }, )(getEndpoint) return err } 

他にも多くのモナドがあります。 配置したい同じ種類の装飾を返す2つの関数を想像してください。 別の例を見てみましょう。


合意されたコンベヤ(同時パイプライン)


チャネルのjoinを定義できます。


 func join(in <-chan <-chan T) <-chan T { out := make(chan T) go func() { wait := sync.WaitGroup{} for c := range in { wait.Add(1) go func(inner <-chan T) { for t := range inner { out <- t } wait.Done() }(c) } wait.Wait() close(out) }() return out } 

ここには、タイプTチャネルを提供するinチャネルがありますT まず、 outチャネルを作成し、ゴルーチンを実行outチャネルを提供し、次にそれを返します。 ゴルーチン内では、から読み取る各チャネルに対して新しいゴルーチンを起動inます。 これらのゴルーチンは、入力データを1つのストリームに結合out 、着信イベントを送信outます。 最後に、待機グループを使用して、すべての入力データを受信したため、出力チャネルが閉じていることを確認します。


つまり、すべてのTチャネルをinから読み取り、それらをoutチャネルに渡します。


Go以外のプログラマーの場合: cをパラメーターとして内部goroutineに渡す必要があります。これは、 cがチャネル内の各要素の値を取る唯一の変数だからです。 これは、値をパラメーターとして渡すことで値のコピーを作成する代わりに、クロージャー内で使用するだけで、おそらく最新のチャネルからしか読み取れないことを意味します。 これはGoプログラマーによくある間違いです。


パイプを返す関数のcompose関数を定義できます。


 func compose(f func(A) <-chan B, g func(B) <-chan C) func(A) <-chan C { return func(a A) <-chan C { chanOfB := f(a) return join(fmap(g, chanOfB)) } } 

また、 join実装方法により、ほとんど何の理由もなく一貫性が得られます。


 func toChan(lines []string) <-chan string { c := make(chan string) go func() { for _, line := range lines { c <- line } close(c) }() return c } func wordsize(line string) <-chan int { removePunc := strings.NewReplacer( ",", "", "'", "", "!", "", ".", "", "(", "", ")", "", ":", "", ) c := make(chan int) go func() { words := strings.Split(line, " ") for _, word := range words { c <- len(removePunc.Replace(word)) } close(c) }() return c } sizes := compose( toChan([]string{ "Bart: Eat my monads!", "Monads: I don't think that's a very good idea.", "Lisa: If anyone wants monads, I'll be in my room.", "Homer: Mmm monads", "Maggie: (Pacifier Suck)", }), wordsize, ) total := 0 for _, size := range sizes { if size == 6 { total += 1 } } // total == 6 

少ない身振り


このモナドの説明は実質的に指で行われます。読みやすくするために、多くのことを意図的に省略しました。 しかし、私が話したいことは他にもあります。


技術的には、前の章で定義したレイアウト関数はKleisli Arrowと呼ばれます。


 type kleisliArrow = func(func(A) M<B>, func(B) M<C>) func(A) M<C> 

人々がモナドについて話すとき、彼らはめったにKleisli Arrowに言及せず、私にとってこれはモナドの本質を理解する鍵になりました。 運がよければ、彼らはfmapjoinを使ってその本質を説明しますが、もしあなたが不運なら、私のように、彼らはbind関数を使ってあなたに説明します。


 type bind = func(M<B>, func(B) M<C>) M<C> 

なんで?


bindはHaskellの関数なので、 bindモナドと見なしたい場合は、タイプに実装する必要があります。


compose関数の実装を繰り返します。


 func compose(f func(A) M<B>, g func(B) M<C>) func(A) M<C> { return func(a A) M<C> { mb := f(a) mmc := fmap(g, mb) mc := join(mmc) return mc } } 

bind関数が実装されている場合、 fmapjoin代わりに単に呼び出すことができます。


 func compose(f func(A) M<B>, g func(B) M<C>) func(A) M<C> { return func(a A) M<C> { mb := f(a) mc := bind(mb, g) return mc } } 

これは、 bind(mb, g) = join(fmap(g, mb))意味します。


リストのbind関数の役割は、言語concatMapまたはflatMap応じて実行されます。


 func concatMap([]A, func(A) []B) []B 

気配りのある外観


Goで、 bindKleisli Arrow区別が薄れ始めていることがわかりました。 Goはタプルでエラーを返しますが、タプルは第1レベルのオブジェクトではありません。 たとえば、インライン化によってfの結果をg渡すことができないため、このコードはコンパイルされません。


 func f() (int, error) { return 1, nil } func g(i int, err error, j int) int { if err != nil { return 0 } return i + j } func main() { i := g(f(), 1) println(i) } 

私はこのように書かなければなりません:


 func main() { i, err := f() j := g(i, err, 1) println(j) } 

または、関数は第1レベルのオブジェクトであるため、 gに関数を入力として使用させます。


 func f() (int, error) { return 1, nil } func g(ff func() (int, error), j int) int { i, err := ff() if err != nil { return 0 } return i + j } func main() { i := g(f, 1) println(i) } 

しかし、それはbind関数を意味します:


 type bind = func(M<B>, func(B) M<C>) M<C> 

エラー固有:


 type bind = func(b B, err error, g func(B) (C, error)) (C, error) 

このタプルを関数に変換するまで使用するのは不快です:


 type bind = func(f func() (B, error), g func(B) (C, error)) (C, error) 

よく見ると、返されたタプルも関数であることがわかります。


 type bind = func(f func() (B, error), g func(B) (C, error)) func() (C, error) 

もう一度よく見ると、これは、 fゼロのパラメーターを受け取るだけのcompose関数であることがfます。


 type compose = func(f func(A) (B, error), g func(B) (C, error)) func(A) (C, error) 

タダム! 私たちはKleisli Arrowを手に入れました。


 type compose = func(f func(A) M<B>, g func(B) M<C>) func(A) M<C> 

おわりに


装飾型の助けを借りたモナドは、バートシンプソンに罰せられることなく、スケートボードに乗って正確にボールを投げることができるように、関数のレイアウトのルーチンロジックを隠しています。



Goでモナドやその他の関数型プログラミングの概念を試してみたい場合は、 GoDeriveコードジェネレーターでこれを行うことができます。


警告 関数型プログラミングの重要な概念の1つは不変性です。 これにより、プログラムの作業が簡素化されるだけでなく、コンパイルを最適化することもできます。 Goでは、不変性をエミュレートするために多くの構造をコピーする必要があり、パフォーマンスが低下します。 関数型言語は、不変性に依存し、それらを再度コピーするのではなく、常に古い値を参照することができるため、これを回避します。


関数型プログラミングを本当にやりたい場合は、 Elmに注意してください。 これは、フロントエンド開発用の静的に型指定された関数型プログラミング言語です。 関数型言語の場合、Goが命令型の単純なものであるように、学習も簡単です。 私は1日ガイドを書き、その夜は生産的に仕事を始めることができました。 言語の作成者は、モナドに対処する必要さえなくして、その研究を簡単にしようとしました。 個人的には、Goのバックエンドと組み合わせてElmのフロントエンドを書くのが好きです。 そして、両方の言語がすでに退屈していても、心配しないでください。まだたくさんの興味深いことがあります。Haskellがあなたを待っています。



Source: https://habr.com/ru/post/J339426/


All Articles