Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrong flatMap definition? (without world version) #1

Closed
tanagumo opened this issue Jun 15, 2017 · 8 comments
Closed

Wrong flatMap definition? (without world version) #1

tanagumo opened this issue Jun 15, 2017 · 8 comments

Comments

@tanagumo
Copy link

tanagumo commented Jun 15, 2017

You define IO.flatMap as below. (https://github.com/akimichi/functionaljs/blob/master/bin/io_without_world.js#L48-L53)

flatMap: (instanceA) => {
  var self = this;
  return (actionAB) => { // actionAB:: a -> IO b
    return self.unit(self.run(actionAB(self.run(instanceA))));
  };
}

But this definition evaluates self.run(actionAB(self.run(instanceA)) eagerly.
For example, the code below displays hello monad before executing IO.run(e).

// putStr :: String => IO[Unit]
const putStr = (s) => IO.flatMap(IO.unit(s))((s) => {
  console.log(s);
  return IO.unit();
});
const e = putStr('hello monad'); // display 'hello monad' at this point
IO.run(e); // shoud be displayed at this point

So I think the definition should be as below.

flatMap: (instanceA) => {
  const self = this;
  return (actionAB) => { // actionAB:: a -> IO b
    return () => self.run(actionAB(self.run(instanceA)))
  };
}

https://github.com/akimichi/functionaljs/blob/master/bin/io_without_world.js#L48-L53
にて、IO.flatMapは以下のように定義されています。

flatMap: (instanceA) => {
  var self = this;
  return (actionAB) => { // actionAB:: a -> IO b
    return self.unit(self.run(actionAB(self.run(instanceA))));
  };
}

しかし、この定義ではself.run(actionAB(self.run(instanceA))を正格評価してしまうと思います。
(正格評価という表現が適切か分かりませんが、IO.flatMap(instanceA)(actionAB)の時点で評価されます。)
例えば、以下のコードではIO.run(e)の実行前にhello monadが表示されます。

// putStr :: String => IO[Unit]
const putStr = (s) => IO.flatMap(IO.unit(s))((s) => {
  console.log(s);
  return IO.unit();
});
const e = putStr('hello monad'); // この時点で'hello monad'が表示されます
IO.run(e); // 実際には、この時点で表示されるべきかと思います

そのため、flatMapの定義としては以下のほうが適切かと思いますが如何でしょうか。

flatMap: (instanceA) => {
  const self = this;
  return (actionAB) => { // actionAB:: a -> IO b
    return () => self.run(actionAB(self.run(instanceA)))
  };
}

よろしくお願いします。

@akimichi
Copy link
Owner

Thank you for your contribution.
Could you give me some time for investigating this issue?
And, shall we discuss this matter in Japanese? Because almost all the readers of this book should be Japanese.
ちょっと検討しますので、少し時間をください。
それから、今後は日本語で返答させてください。他の読者の方にもそのほうが便利でしょうから。

@tanagumo
Copy link
Author

starをつけてらっしゃる方の中に韓国籍の方がいらっしゃるようでしたので慣れない英語で書きました。日本語での質問も記載しておきました。よろしくお願いします。

@akimichi
Copy link
Owner

akimichi commented Jun 16, 2017

ちょっと調べてみました。
問題となっている以下のファイルは、執筆途中で試験的に書いたものでして、最終的にボツになったものです。
https://github.com/akimichi/functionaljs/blob/master/bin/io_without_world.js
ボツにした理由は、たぶん指摘されたような問題点があったのと、thisポインタを使っていたからだと思います。

出版された書籍では bin/cat_without_world.js のファイルに定義されたflatMap関数を採用しています。

        /* flatMap:: IO[T] => FUN[T => IO[U]] => IO[U] */
        flatMap : (instanceA) => {
          return (actionAB) => { // actionAB:: a -> IO[b]
            return IO.unit(IO.run(actionAB(IO.run(instanceA))));
          };
        },

この定義は、tanagumoさんが最後に掲載したflatMap関数と実質的に同一のものになっていると思いますが、いかがでしょうか?

ところで、このレポジトリには執筆の途中でボツになったコードがいくつかまぎれこんでいます。
特に binとlibのディレクトリに顕著です。
まぎらわしいので、少しずつ整理していくことにします。

追伸:
たしかに star をつけてくれた方のなかに、韓国と台湾の方がいるのに驚きました。
ただ私もそんなに英語が得意なわけではないので、今後も日本語で返答させてください。

@tanagumo
Copy link
Author

tanagumo commented Jun 20, 2017

間が空いてしまいまして申し訳ありません。

まずIOモナドについて自分の理解を述べさせて頂きたいのですが、IO[A]がもつ副作用はIO[A] => Aという風にIOモナドのコンテキストから中の値を取り出す際に発生するという認識です。
書籍で登場するseqのような関数を用いて、IO[A] => IO[B] => IO[B]のようにIOモナドを合成した際にも合成されたIOモナドからその中の値を取り出す際に、合成する前の各IOモナドがもつ副作用が逐次的に発生すると理解しています。

実際にghciで確認する限りでは自分の理解と同じ挙動をしていると思います。

Prelude> :t (>>)
(>>) :: Monad m => m a -> m b -> m b
Prelude>
Prelude> :t putChar
putChar :: Char -> IO ()
Prelude>
Prelude> let {
Prelude| ab :: IO ();
Prelude| ab = putChar 'a' >> putChar 'b' >> putChar '\n';
Prelude| }
Prelude> -- abは3つのputCharを合成したIOモナドだが、副作用(画面表示)はこの時点では発生しない
Prelude> ab
ab -- ghciなのでabを評価しようとした際にIOモナドの中身が取り出され、その時点で副作用が発生する。

ただ、このIOモナドの副作用の発生タイミングがhaskellに限定的な話なのかIOモナド一般に求められる性質なのかについて私には知見がありません。以下、自分の理解が正しい仮定の元で記載させて頂きます。

https://github.com/akimichi/functionaljs/blob/master/bin/cat_without_world.js で定義されているflatMapも以前に私が記載したflatMapも型レベルで見ると同じなのですが副作用の発生タイミングが異なります。bin/cat_without_world.jsの定義で上記のghciと同じ事をやってみますと

> const ab = IO.flatMap(IO.putChar('a'))(() =>
    IO.flatMap(IO.putChar('b'))(() =>
      IO.unit()
    )
  );
abundefined // この時点で副作用(画面表示)が発生してしまう。
> IO.run(ab);
undefined   // IOモナドから値を取り出しても副作用は発生しない。

となります。ちなみにbin/cat_without_world.jsのIO.putCharの定義は

putChar: (character) => {
  /* 1文字だけ画面に出力する */
  process.stdout.write(character); 
  return IO.unit(null);
}

となっており、型レベルではChar => IO[Unit]ですが、putCharを適用した時点で画面表示されてしまうので以下が正しいのではないかと思います(実際別のファイルでは以下のような定義になっていた気がします)。

putChar: (character) => () => {
  process.stdout.write(character); 
  return null;
}

但し、putCharの定義を上記のように修正してもbin/cat_without_world.jsのflatMapの定義のままだとIOモナドから値を取り出す時点まで副作用の発生を遅延させる事はできません。

IOモナドの性質として、IOモナドから値を取り出す時点で副作用を発生させるべきなのであれば先日記載させて頂いた定義である必要があるかと思いますが如何でしょうか。

ちなみにですが、参照しているブランチはmasterです。

長文になってしまいましたがよろしくお願いします。

@akimichi
Copy link
Owner

こちらでも同じような結果になることを確認しました。
たしかに書籍のコードにはご指摘のような不具合があります。
IOモナドの挙動については私もtanagumoさんと同じつもりですが、私の理解にどこか不十分な点があります。
検討してみますが、なかなか手強そうです。
この問題は関数の評価戦略に関係しているのでしょうか?
原因を突きとめましたら、教えていただけないでしょうか。

@tanagumo
Copy link
Author

https://github.com/akimichi/functionaljs/blob/master/bin/cat.js のIOの定義は期待すべきIOモナドの挙動になっていると思います。

実際

const readAndPrint = IO.flatMap(IO.readFile('./somefile.txt'))((content) =>
  IO.flatMap(IO.println(content))(() =>
  	world => IO.unit()(world)
  )
); // この時点では副作用(ファイルからのreadと画面表示)は発生しない。

const dummyWorld = null; // 便宜上の外界
IO.run(readAndPrint)(dummyWorld); // この時点で副作用(ファイルの内容の画面表示)が発生する。

上記のコードはIOモナドの定義時ではなく、IO.runによってIOモナドから値を取り出すまで副作用の発生が遅延されており期待する挙動になっているかと思います。

worldを明示するバージョンとwithout_worldバージョンのflatMapですが

worldを明示するバージョン

flatMap: (instanceA) => {
  return (actionAB) => { // actionAB:: A -> IO[B]
    return (world) => {  // 引数worldは現在の外界
      /* 現在の外界のなかで instanceAのIOアクションを実行する */
      var newPair = instanceA(world);
      return pair.match(newPair,{
        cons: (value, newWorld) => {
          /*
             新しい外界のなかで、actionAB(value)で作られた
             IOアクションを実行する
          */
          return actionAB(value)(newWorld);
        }
      });
    };
  };
},

without_worldバージョン

flatMap: (instanceA) => {
  return (actionAB) => { // actionAB:: A -> IO[B]
    /* instanceAのIOアクションを実行し、
       続いて actionABを実行する */
    return actionAB(IO.run(instanceA));
  };
},

となっています。worldを明示するバージョンはflatMap(instanceA)(actionAB)の結果がworldを引数にとる関数になるのでworldに適用されるまでは何も起きません。without_worldバージョンはflatMap(instanceA)(actionAB)の結果はactionAB(IO.run(instanceA))の評価結果になります。actionAB自体はコンテキストから取り出した値に適用されて新たなIOモナドを生成する関数のため、型レベルでの不整合は起きていませんが、JavaScriptは実引数を正格評価するためIO.run(instanceA)が正格評価されてこの時点で副作用が発生してしまっています。

そこで、この箇所を() => IO.run(actionAB(IO.run(instanceA)))に変更する事で明示的に関数呼び出しされるまではIO.run(actionAB(IO.run(instanceA)))が評価されなくなり、副作用の発生を遅延させる事ができると思います。

結果的にwithout_world版とworldを明示するバージョンの実質的な際はpairの有無だけかと思います。具体的にはwithout_world版のIOの定義は下記の様になるのではないでしょうか。

IOの定義(議論上必要な箇所のみ抜粋)

// type IO[A] = FUN[() => A]

const IO = { 
  // unit :: A => IO[A]
  unit: (any) => (() => any),
  
  // run :: IO[A] => A
  run: (instance) => instance(),
  
  // flatMap :: IO[A] => FUN[A => IO[B]] => IO[B]
  flatMap: (instanceA) => actionAB => (() => {
  	const value = IO.run(instanceA);
  	return IO.run(actionAB(value));
  }),
  
  // putChar :: CHAR => IO[Unit]
  putChar: character => (() => {
  	process.stdout.write(character);
  	return undefined;
  }),
};

よろしくお願いします。

@akimichi
Copy link
Owner

akimichi commented Jun 23, 2017

tanagumoさん、
とても詳しい説明をしていただき、ありがとうございました。
たしかに無名関数に副作用を封じこめて評価を遅らせると、希望通りの挙動が実現できます。
やはり問題の源泉は、JavaScriptが引数を正格評価する点にあるのですね。
ついつい型の整合性にばかり気をとられてしまい、評価の順序をないがしろにしていました。
考えてみれば型は、評価の順序までを保証する仕組みではありませんでした。

さて問題は、不覚にも間違った内容を書籍に載せてしまった点です。
もともとこの書籍の正誤表は、正式には http://www.ric.co.jp/book/error/error1059.html に掲載されることになっています。
ただこちらはごく単純な誤植などを掲載するものなので、今回のような長文の訂正には適していないかも知れません。
かといってgithubのこのサイトに詳細な訂正を載せてしまうと、書籍の正誤表が2箇所に分散されてしまうという難点が生じてしまいます。
出版社の方とも相談してみることにします。

@tanagumo
Copy link
Author

こちらこそご回答ありがとうございました。
自分の中でも今回質問させて頂いた事で理解が深まった気がします。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants