MostlyAdequate / mostly-adequate-guide

Mostly adequate guide to FP (in javascript)
Other
23.39k stars 1.86k forks source link

Ch12: array is not a monad #581

Open antonklimov opened 4 years ago

antonklimov commented 4 years ago

sequence(Either.of, [Either.of('wing')]); // Right(['wing']) : how is this supposed to work? Array does not have .sequence. sequence(Either.of, new List([Either.of("wing")])) might work here.

Later

const firstWords = compose(join(' '), take(3), split(' '));

// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);

traverse(Task.of, tldr, ['file1', 'file2']);

Two problems here. First is again with array being treated as a monad. And second that join which is defined in support\index.js is a monadic join

// join :: Monad m => m (m a) -> m a
const join = (m) => m.join();

whereas here the join needs to be an array join like:

// arrayJoin :: String -> [String] -> String
const arrayJoin = curry((sep, arr) => arr.join(sep));
exports.arrayJoin = arrayJoin;
KtorZ commented 4 years ago

how is this supposed to work? Array does not have .sequence.

It is supposed to work because you only need the internal object to be an Applicative to write sequence / traverse. Here, the array ought to be treated as a Foldable and the Either a as the Applicative. Yet, I've just checked and indeed, the definition of the pointfreesequence is bonkers at the moment.

First is again with array being treated as a monad.

Nope, array is being treated as a Foldable.

And second that join which is defined in support\index.js is a monadic join

Right! We have a name clash here, and this one should be intercalate

antonklimov commented 4 years ago

It seems that sequence can be defined as: const sequence = curry((of, f) => traverse(of, identity, f)); and traverse can be made aware of arrays like:

const traverse = curry((of, fn, f) =>
  Array.isArray(f)
    ? f.reduce((innerF, a) => fn(a).map(append).ap(innerF), of([]))
    : f.traverse(of, fn)
);

Would that work?

KtorZ commented 4 years ago

Yes indeed! That sounds a bit more sound that extending the Array prototype with a "traverse" method ^.^

deleite commented 4 years ago

@antonklimov Even when I changed the array with the snippet you put. I still can't make example work. I get a buffer but I try to partially the readfile with the encoding suddenly the readfile task blows up. I am sure I am doing something wrong. This might be unrelated so sorry if the case. Here is the example.

const readFile = filename => new Task((reject, result) => {
  readFile(filename, (err, data) => (err ? reject(err) : result(data)));
});

// readDir :: String -> Task Error (List String)
const readDir = path => new Task((reject, result) => {
  fs.readdir(path, (err, data) => (err ? reject(err) : result(data)));
});
// readFirst :: String -> Task Error (Maybe String)
const readFirst = compose(
  chain(traverse(Task.of, readFile('utf-8'))),
  map(safeHead),
  readDir,
);

The little readfile I could not find in the book by the way. if there supposed to be one.

antonklimov commented 4 years ago

This is not related to array, and first of all, in support.js there are the following definitions:

const readdir = function readdir(dir) {
  return Task.of(['file1', 'file2', 'file3']);
};

const readfile = curry(function readfile(encoding, file) {
  return Task.of(`content of ${file} (${encoding})`);
});

Your readFile takes only 1 parameter and is not curried. So readFile('utf-8') is not a function which is required in traverse but rather a Task with "utf-8" treated as a file name. Also note that for fs.readFile (I suppose that is what you wanted to use) the name is the first, and the option will be the second, which you did not provide in your definition. Another problem with your code is that rs.readdir can return subdirectories and fs.read will fail on that. So you would need to change your definition of readdir to filter out the directories. The easiest fix for readFile would be:

const readFile = (encoding) => (filename) =>
  new Task((reject, result) => {
    fs.readFile(filename, encoding, (err, data) =>
      err ? reject(err) : result(data)
    );
  });
dotnetCarpenter commented 3 years ago

oh my.. I have spend hours looking at this and I swear it melt my brains up to the point where I have to put my laptop aside to regain sanity.

I will try to break it down, for you and for myself.

The disturbing lines in chapter 12 is this:

Let's rearrange our types using sequence:

   sequence(List.of, Maybe.of(['the facts'])); // [Just('the facts')]
   sequence(Task.of, new Map({ a: Task.of(1), b: Task.of(2) })); // Task(Map({ a: 1, b: 2 }))
   sequence(IO.of, Either.of(IO.of('buckle my shoe'))); // IO(Right('buckle my shoe'))
   sequence(Either.of, [Either.of('wing')]); // Right(['wing'])
   sequence(Task.of, left('wing')); // Task(Left('wing'))
// sequence :: (Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a)
const sequence = curry((of, f) => f.sequence(of));

Line 1

sequence(List.of, Maybe.of(['the facts'])), // [Just('the facts')]

This makes total sense. The List is an ADT for Array. Maybe.of (Array (String)) becomes Just (Array (String)), which gets turned inside out by sequence to Array (Just (String)). The last type could also be written as [Just (String)].

Line 2

Actually, I am going to skip line 2 because that is the one I understand the least...

Line 3

sequence(IO.of, Either.of(IO.of('buckle my shoe'))); // IO(Right('buckle my shoe'))
console.log( sequence(IO.of, Either.of(IO.of('buckle my shoe'))).unsafePerformIO() ) // TypeError

Again, this makes total sense but this time we get a TypeError - Cannot read property 'unsafePerformIO' of undefined. Turns out that Right.traverse is missing a return (I got a PR for that). With the return added, we get Right('buckle my shoe').

  1. IO.of ('buckle my shoe') returns the Applicative Functor IO which holds a String.
  2. Either.of (IO (String)) returns Right (IO (String)).
  3. sequence's arguments are IO.of ==> x => new IO(() => x) and Right (IO (String)).
  4. The body of sequence is then, Right (IO (String)).sequence(x => new IO(() => x)).
  5. Right.sequence's body is then, this.traverse(x => new IO(() => x), identity).
  6. Right.traverse does not use the first argument but only the second argument fn, which is identity. So the body becomes, identity(this.$value).map(Either.of) ==> IO (String).map(Either.of)
  7. IO.map becomes, new IO (() => Either.of (IO (String).unsafePerformIO())).

The tranformation is (new IO (() => Either.of (IO (String).unsafePerformIO()))).unsafePerformIO() ==> Right (String). When we execute this newly formed IO we execute the following steps:

  1. (new IO (() => Either.of (IO (String).unsafePerformIO()))).unsafePerformIO()
  2. Executing the inner Applicative:
    1. Either.of (IO (String).unsafePerformIO())
    2. Either.of (String)
    3. Right (String)
  3. (new IO (() => Right (String))).unsafePerformIO()
  4. IO (Right (String)).unsafePerformIO()
  5. Right (String)

Line 4

sequence(Either.of, [Either.of('wing')]), // TypeError

As noted in the original post, that line does not make sense. It could be sequence(List.of, Either.of(['wing'])) // [Right('wing')] <- Invert list in Either to List (Right('wing')). Or sequence(Either.of, new List([Either.of("wing")])) <- Invert list of Eithers to Either ({$value: ['wing]}). The latter introduced an object, which holds a List (String).

Something is afoul here. Perhaps a bug? I now see that List (a) will print a as {$value: } via [util.inspect.custom], regardless of the actual Type it holds. So Right(2) will show as {$value: 2} etc.

[util.inspect.custom]() {
  return `List(${inspect(this.$value)})`;
}

I think line 4 is suppose to be:

sequence(List.of, Either.of(['wing'])), // List (Right('wing'))

Line 5

// left :: a -> Either a b
const left = a => new Left(a);

sequence(Task.of, left('wing')), // Task(Left('wing'))

sequence(Task.of, left('wing'))
    .fork(console.error.bind(console, 'Error:'), console.log), // Left('wing')

Line 5, makes total sense but the purpose evades me. Why would I want to use sequence to wrap Left (String) in a Task? Task.of(left('wing')) will yield the exact same result but much cleaner and faster.

Line 2

sequence(Task.of, new Map({ a: Task.of(1), b: Task.of(2) })), // Task(Map({ a: 1, b: 2 }))

// but..

sequence(Task.of, new Map({ a: Task.of(1), b: Task.of(2) }))
    .fork(console.error.bind(console, 'Error:'), console.log), // TypeError: Map.of is not a function

It's important to note that Map is shadowed by class Map in support/index.js. I will not try to outline the execution here. When I look at the class, I see that Map.insert is using Map.of that does not exist. It makes me think that this class has never actually been executed but is a piece of theory that I can not hope to use and experiment with.

It would be super helpful, in learning the concept of Traversable and being comfortable using Traversables, if the examples would work as advertised. I'm still not knowledgeable enough to make this work on my own and I feel stuck in an otherwise great book on ADTs.

dotnetCarpenter commented 3 years ago

With the exception of line 5; I think I understood it all now. Please see #605 and correct me, if I'm wrong.