0

I’ve been trying to implement an Apply/Applicative type based on Javascript Functor, Applicative, Monads in pictures and the blog series Fantas, Eel, and Specification.

I think I’m making good progress, but I ran in to a case I couldn’t really find in any of the articles.

TL;DR Question

If lift2 is

f -> A.of(x) -> A.of(y) -> A.of(f (x) (y))     -or-  A(y).ap(A(x).map(f))

what is the theory/name/type behind

A.of(f) -> A.of(x) -> A.of(y) -> A(f (x) (y))  -or-  A(y).ap(A(x).ap(A(f)))

Introduction

I’m working with knockout.js, which gives me observable values. I’m trying to use them in a kind of functional fashion, extending them when needed.

First, I implemented map to make myself a functor:

ko.subscribable.fn.map = function(f) {
  // note: calling without arguments is knockout's 
  //       way of "unwrapping"
  return ko.pureComputed(
    () => f(this())
  );
}

This allows me to do things like:

// "Pure" part describing my app
const myValue = ko.observable(2);
const doubleThat = myValue.map(x => x * 2);

// In- and output (IO?)
doubleThat.subscribe(console.log);
myValue(3); // Logs 6

Then, I ran in to the problem of working with functions that take multiple arguments. For example:

const filter = (pred, xs) => xs.filter(pred);

I solved my issues by implementing ap and currying my functions:

ko.subscribable.fn.ap = function(sf) {
  return ko.pureComputed(
    () => sf () (this()) 
  );
};

const filter = pred => xs => xs.filter(pred);

With these changes in place, I can do:

const odd = x => x % 2 === 1;

const myPred = ko.observable(odd);
const myValues = ko.observable([ 1, 2, 3 ]);

const myFilter = myPred.map(filter);
const myResult = myValues.ap(filter); // S([ 1, 3 ])

The definition of lift2 gives me another way of writing the same thing.

const myResult = lift2 (filter) (myPred) (myResult)

So far, so good. I can use the dot-calls if the interim result is reusable, and a liftN call if I only care about the final outcome.

The problem

The liftN way of chaining one map with N - 1 calls to ap only works if I use plain functions. In my app however, I often deal with functions that are themselves wrapped in subscribables! For example:

const sum = x => y => x + y;

const mathStrategy = ko.observable(sum);
const v1 = ko.observable(2);
const v2 = ko.observable(3);

My attempts

Chaining works, but quickly gets very hard to understand.

// Ugly...
const myResult = v2.ap(v1.ap(mathStrategy)); // S(5)

I can use liftN, but only if I make sure my first function is id.

// Also ugly...
const id = x => x;
const myResultL = lift3 (id) (mathStrategy) (v1) (v2); // S(5)

My questions

  • If lift2 handles f -> A.of(x) -> A.of(y) -> A.of(f (x) (y)), what is the theory/name/type behind A.of(f) -> A.of(x) -> A.of(y) -> A(f (x) (y))
  • If such a thing does not really “exist”, would it be okay to write an implementation of ap that unwraps A(f) on the go? (i.e. f => ko.unwrap (f) (x))

Code example

Object.assign(ko.subscribable, {
  
  of: function(x) {
    return ko.pureComputed(() => x)
  }
  
});

Object.assign(ko.subscribable.fn, {

  map: function(f) {
    return ko.pureComputed(() => f(this()));
  },
  
  ap: function(sf) {
    return ko.pureComputed(() => sf () (this()));
  },
  
  toString: function() {
    return `S(${JSON.stringify(this())})`;
  }

});

// Example code:
const sum = x => y => x + y;
const mult = x => y => x * y;

const mathStrategy = ko.observable(sum);
const v1 = ko.observable(1);
const v2 = ko.observable(3);

const result = v2.ap(v1.ap(mathStrategy));

console.log(result); // S(4)
v1(2);
mathStrategy(mult);
console.log(result); // S(6)
.as-console-wrapper { min-height: 100% !important; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

Note: This is kind of related to a question I asked earlier. Back then, I was focusing on lazily unwrapping A(x) and A(y), which I have given up on by now .

user3297291
  • 22,592
  • 4
  • 29
  • 45
  • Sorry, I don't get your notation at all. What is this `->` supposed to mean, why is it mixed with `A.of`? Is this supposed to represent a type, supposed to represent an implementation, both, or something else? – Bergi Sep 26 '19 at 18:23
  • Your `ap` examples don't make sense. Did you mean `myResult = myValues.ap(myFilter)` (or `myFilter.ap(myValues)`, the OOP parameter order isn't exactly clear), and `myResult = lift2(filter)(myPred)(myValues)`? – Bergi Sep 26 '19 at 18:28
  • "*`liftN` only works if I use plain functions, however I often deal with functions that are themselves wrapped in subscribables*" - well that's the only purpose of `liftN`, to lift plain functions into the applicative. If you already have wrapped functions, you don't need to lift them any more, you only need to use `ap` - and you should not try to use a `lift` function for no reason. – Bergi Sep 26 '19 at 18:31
  • Sorry for not being clear. It's just that I don't like doing `d.ap(c.ap(b.ap(a.ap(sum4))))` and figured some functional programmer probably already solved this. I'm trying to self-learn here and usually find out way too late that I'm solving issues that have been named, solved and described by other people before. – user3297291 Sep 27 '19 at 09:26
  • If you flip `ap` the right way round, you can do `sum4.ap(a).ap(b).ap(c).ap(d)`. That's what everyone uses, that's why there is no dedicated function for this and no name either. If you insist on an `apply2`/`ap2` function (the name is just my guess, but it seems sensible), you can build it yourself with `lift3(id)` or some weird point-free composition of `ap`s. – Bergi Sep 27 '19 at 10:57
  • My reference article states `ap :: Apply f => f a ~> f (a -> b) -> f b`, which I understood to mean the function needs to be passed to `ap`, not the value ([source](http://www.tomharding.me/2017/04/10/fantas-eel-and-specification-8/)). But if either way is fine, I think I know enough to continue. Thanks for the help, these "does a better thing exist" questions are hard to ask... – user3297291 Sep 27 '19 at 11:12

1 Answers1

1

I think the question you meant to ask, with proper notation for the types, is

If lift2 handles Apply f => (x -> y -> z) -> f x -> f y -> f z, what is the theory/name behind Apply f => f (x -> y -> z) -> f x -> f y -> f z?

Such a function is rare, because everyone just uses ap or the respective infix operator, like in Haskell

let az = af <*> ax <*> ay

However, I saw some two references on the internet to it being called ap2 [1][2], which makes sense as it is analogous to lift becoming lift2.

You can write this helper function as

const ap2 = af => ax => ay => ap(ap(af, ax), ay)

or like you already did

const ap2 = lift3(id)
Bergi
  • 630,263
  • 148
  • 957
  • 1,375