MostlyAdequate / mostly-adequate-guide

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

Chapter 10, Part Laws. We don't need liftA2(concat). #628

Open alisajadih opened 2 years ago

alisajadih commented 2 years ago

In chapter 10 on part laws. there is a code that demonstrates "applicatives are close under composition".

const tOfM = compose(Task.of, Maybe.of);
liftA2(liftA2(concat), tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));
// Task(Maybe(Rainy Days and Mondays always get me down))

I think we could remove liftA2 wrapper for concat function, It's useless. liftA2(concat, tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));

sevillaarvin commented 1 year ago

EDIT2: Nvm, just saw this https://github.com/MostlyAdequate/mostly-adequate-guide/pull/252, and realized that I needed to use the "pumped up" version of curry, as defined in exercises/support.js:

// NOTE A slightly pumped up version of `curry` which also keeps track of
// whether a function was called partially or with all its arguments at once.
// This is useful to provide insights during validation of exercises.
function curry(fn) {
  assert(
    typeof fn === 'function',
    typeMismatch('function -> ?', [getType(fn), '?'].join(' -> '), 'curry'),
  );

  const arity = fn.length;

  return namedAs(fn.name, function $curry(...args) {
    $curry.partially = this && this.partially;

    if (args.length < arity) {
      return namedAs(fn.name, $curry.bind({ partially: true }, ...args));
    }

    return fn.call(this || { partially: false }, ...args);
  });
}

EDIT: So I finally got it working in javascript, but liftA2(concat) needed an additional curry, for reasons I do not understand.

As we can see below, I had to liftA2(curry(liftA2(concat)), x, y) in order for lifting twice to work, otherwise there is an error in liftA2 when calling .ap.

#+begin_src js :noweb no-export :results code
  <<js maybe applicative>>
  <<js task applicative>>
  <<js compose>>
  <<js liftA2>>
  <<js identity>>
  <<js concat>>

  const tOfM = compose(Task.of, Maybe.of)

  const x = tOfM("Hello ")
  const y = tOfM("World")
  const curriedAndLiftedConcat = curry(liftA2(concat))
  const z = liftA2(curriedAndLiftedConcat, x, y)

  ////// ERROR //////
  // NOTE this curry-less function throws an error, why???
  const onlyLiftedConcat = liftA2(concat)
  const a = liftA2(onlyLiftedConcat, x, y)
  let afork
  try {
    afork = a.fork(id, id)
  } catch (error) {
    afork = error.message
  }
  ////// ERROR //////

  return {
    // x,
    // xfork: x.fork(id, id),
    // y,
    // yfork: y.fork(id, id),
    z,
    zfork: z.fork(id, id),
    a,
    afork
  }
#+end_src

#+RESULTS:
#+begin_src js
{
  z: Task { fork: [Function (anonymous)] },
  zfork: Maybe { val: 'Hello World' },
  a: Task { fork: [Function (anonymous)] },
  afork: "Cannot read properties of undefined (reading 'map')"
}
#+end_src

Dependencies implementations:

Maybe ```js #+name: js maybe applicative #+begin_src js class Maybe { constructor(val) { this.val = val } static of(val) { return new Maybe(val) } get isNothing() { return this.val === null || this.val === undefined } map(fn) { return this.isNothing ? this : new Maybe(fn(this.val)) } join() { return this.isNothing ? this : this.val } chain(fn) { return this.map(fn).join() } ap(f) { return f.map(this.val) } } #+end_src ```
Task ```js #+name: js task applicative #+begin_src js :noweb no-export class Task { constructor(fn) { this.fork = fn } static of(val) { return new Task((_reject, result) => result(val)) } map(fn) { <> return new Task( (reject, result) => this.fork( reject, compose(result, fn) ) ) } join() { return new Task((reject, result) => this.fork( reject, x => x.fork(reject, result) )) } chain(fn) { return this.map(fn).join() } ap(f) { return new Task((reject, resolve) => this.fork( reject, x => f.map(x).fork(reject, resolve) )); } } #+end_src ```
compose() ```js #+name: js compose #+name: js compose functional #+begin_src js const compose = (...fs) => (...args) => { return fs.reduceRight( (result, f) => [f.apply(null, result)], args )[0] // alternatively: // return fs.reduceRight((result, f) => f.apply(null, [].concat(result)), args) } #+end_src ```
liftA2() ```js #+name: js liftA2 #+begin_src js :noweb no-export <> const liftA2 = curry((g, f1, f2) => f1.map(g).ap(f2)) #+end_src ```
curry() ```js #+name: js curry #+begin_src js const curry = (f) => { const arity = f.length return (...args) => { if (args.length < arity) { return f.bind(null, ...args) } return f.apply(null, args) } } #+end_src ```
id() ```js #+name: js identity #+begin_src js const id = (x) => x #+end_src ```
concat() ```js #+name: js concat #+begin_src js const concat = curry((a, b) => a.concat(b)) #+end_src ```
=== ORIGINAL QUESTION === Also I'd like to ask, what's the signature of `concat` here? When I try to analyze it, and assume that `concat :: String -> String -> String` (from https://mostly-adequate.gitbook.io/mostly-adequate-guide/appendix_c#concat), then `liftA2(concat, tOfM('Rainy Days and Mondays'), tOfM(' always get me down'))` doesn't work because: by definition, `const liftA2 = curry((fn, a1, a2) => a1.map(fn).ap(a2))` (from https://mostly-adequate.gitbook.io/mostly-adequate-guide/appendix_a#lifta2), which evaluates to `tOfM('Rainy Days and Mondays').map(concat)`, which means `concat` expects a `String` but is given a `Maybe String`, since `tOfM :: a -> Task (Maybe a)`. Is it correct to assume that in this example `concat :: Maybe String -> Maybe String -> Maybe String`?