mostly-adequate-guide icon indicating copy to clipboard operation
mostly-adequate-guide copied to clipboard

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

Open alisajadih opened this issue 3 years ago • 2 comments

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'));

alisajadih avatar Nov 21 '21 08:11 alisajadih

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
#+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
#+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) {
      <<js compose>>

      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()
#+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()
#+name: js liftA2
#+begin_src js :noweb no-export
  <<js curry>>

  const liftA2 = curry((g, f1, f2) => f1.map(g).ap(f2))
#+end_src
curry()
#+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()
#+name: js identity
#+begin_src js
  const id = (x) => x
#+end_src
concat()
#+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?

sevillaarvin avatar Jul 24 '22 06:07 sevillaarvin