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

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

Open
alisajadih opened this issue Nov 21, 2021 · 2 comments
Open

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

alisajadih opened this issue Nov 21, 2021 · 2 comments

Comments

@alisajadih
Copy link

alisajadih commented Nov 21, 2021

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
Copy link

sevillaarvin commented Jul 24, 2022

EDIT2: Nvm, just saw this #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?

@damon314159
Copy link

damon314159 commented Sep 26, 2024

This is a very old issue, but I also was a little confused about this, did some research and played around with the code for a while until I understood. I hope this helps someone reading in the future. The inner liftA2 is required because each lift only lifts into one container, so you need both lifts to raise concat into Task Maybe.

The signature of liftA2 is

(a -> b -> c) -> f a -> f b -> f c

So what you're proposing when you suggested

liftA2(concat, tOfM('Rainy Days and Mondays'), tOfM(' always get me down'))

would boil down to

tOfM('Rainy Days and Mondays').map(concat).ap(tOfM(' always get me down'))

And since tOfM is a Task, map will provide to concat the return type of the Task, which is Maybe String.
concat isn't expecting Maybe String, it's expecting String, so this throws.


However, if you do, as the author writes,

liftA2(liftA2(concat), tOfM('Rainy Days and Mondays'), tOfM(' always get me down'))

then this will become

tOfM('Rainy Days and Mondays').map(liftA2(concat)).ap(tOfM(' always get me down'))

which is perfect, since the call to map provides liftA2(concat) with the type Maybe String, and then ap provides another Maybe String, and so ultimately you are left with something equivalent to

Task.of(
  liftA2(concat)(Maybe.of('Rainy days'))(Maybe.of('always down'))
)

which is exactly what you want

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

3 participants