3

I have a variadic lifting function that allows for flat monadic chains without deeply nested function composition:

const varArgs = f => {
  const go = args =>
    Object.defineProperties(
      arg => go(args.concat(arg)), {
        "runVarArgs": {get: function() {return f(args)}, enumerable: true},
        [TYPE]: {value: "VarArgs", enumerable: true}
      });

  return go([]);
};

const varLiftM = (chain, of) => f => { // TODO: replace recursion with a fold
  const go = (ms, g, i) =>
    i === ms.length
      ? of(g)
      : chain(ms[i]) (x => go(ms, g(x), i + 1));

  return varArgs(ms => go(ms, f, 0));
};

It works but I'd like to abstract from the recursion with a fold. A normal fold doesn't seem to work, at least not along with the Task type,

const varLiftM = (chain, of) => f =>
  varArgs(ms => of(arrFold(g => mx => chain(mx) (g)) (f) (ms))); // A

because the algebra in line A would return a Task for each iteration, not a partially applied function.

How can I replace the non-tail recursion with a fold?

Here is a working example of the current recursive implementation:

const TYPE = Symbol.toStringTag;

const struct = type => cons => {
  const f = x => ({
    ["run" + type]: x,
    [TYPE]: type,
  });

  return cons(f);
};

// variadic argument transformer

const varArgs = f => {
  const go = args =>
    Object.defineProperties(
      arg => go(args.concat(arg)), {
        "runVarArgs": {get: function() {return f(args)}, enumerable: true},
        [TYPE]: {value: "VarArgs", enumerable: true}
      });

  return go([]);
};

// variadic monadic lifting function

const varLiftM = (chain, of) => f => { // TODO: replace recursion with a fold
  const go = (ms, g, i) =>
    i === ms.length
      ? of(g)
      : chain(ms[i]) (x => go(ms, g(x), i + 1));

  return varArgs(ms => go(ms, f, 0));
};

// asynchronous Task

const Task = struct("Task") (Task => k => Task((res, rej) => k(res, rej)));

const tOf = x => Task((res, rej) => res(x));

const tMap = f => tx =>
  Task((res, rej) => tx.runTask(x => res(f(x)), rej));

const tChain = mx => fm =>
  Task((res, rej) => mx.runTask(x => fm(x).runTask(res, rej), rej));

// mock function

const delay = (ms, x) =>
  Task(r => setTimeout(r, ms, x));

// test data

const tw = delay(100, 1),
  tx = delay(200, 2),
  ty = delay(300, 3),
  tz = delay(400, 4);

// specialization through partial application

const varAsyncSum =
  varLiftM(tChain, tOf) (w => x => y => z => w + x + y + z);

// MAIN

varAsyncSum(tw) (tx) (ty) (tz)
  .runVarArgs
  .runTask(console.log, console.error);

console.log("1 sec later...");

[EDIT] As desired in the comments my fold implementation:

const arrFold = alg => zero => xs => {
  let acc = zero;

  for (let i = 0; i < xs.length; i++)
    acc = alg(acc) (xs[i], i);

  return acc;
};
  • That is a lot of minified code to go over. Could you simplify the example? – XCS Jun 02 '19 at 17:39
  • @cristy thats not minified, thats "functional programming" :) – Jonas Wilms Jun 02 '19 at 17:56
  • 2
    Well, naming your parameters like `ms`, `g`, `x`, `f`, `fm`, `of`, `tx`, `res`, `rej` and then using all of them (almost at once) for me looks like unnecessary minification, making the code much harder to read unless you know what it is about. – XCS Jun 02 '19 at 17:59
  • 3
    @Cristy most of those, if not all, are rather idiomatic in functional programming. Similar to how you'd name a variable `i` if it's a counter or index of some sort. `f` - function, `g` also a function (alphabetically after `f`, akin to `j` being used as a counter after `i` is declared), `fm` - function returning a monad, `ms` - monads plural, so an array, `of` function doing a type lift into a monad. `x` - generic name for an input variable. `res` and `rej` - result (success) and rejection (failure). `tx`, `ty`, `tz` - task1, task2, task3. – VLAZ Jun 02 '19 at 18:16
  • The short names just reflect how general the code is. You can use `varLiftM` with any monad, for instance. To complete the name legend by @VLAZ: `mx` monadic value, `ms` monadic values, `fm` monadic action (function that returns a monad). `tx` just means a value wrapped in a type without telling something about the constraints (i.e. if it is monadic, applicative, functorial, traversal, foldable, etc). –  Jun 02 '19 at 18:38
  • I'd recommend to use `Object.assign` instead of `Object.defineProperties`, so that you don't have to worry about property descriptors – Bergi Jun 02 '19 at 19:15
  • The parameters of `chain` seem to be swapped, usually the function comes first? Also, could you please paste your definition of `arrFold`? – Bergi Jun 02 '19 at 19:17
  • @Bergi I started with `Object.assign`, but this strictly calls the getter during copying... Thanks with the prop descriptor hint, fixed it. –  Jun 02 '19 at 20:05
  • @bob Oh, now I see, it actually is a getter doing the call, I was thinking you had a getter returning a function… – Bergi Jun 02 '19 at 20:30

2 Answers2

0

That of call around arrFold seems a bit out of place.

I'm not sure whether your arrFold is a right fold or left fold, but assuming it is a right fold you will need to use continuation passing style with closures just as you did in your recursive implementation:

varArgs(ms => of(arrFold(g => mx => chain(mx) (g)) (f) (ms)))

becomes

varArgs(ms => arrFold(go => mx => g => chain(mx) (x => go(g(x)))) (of) (ms) (f))

With a left fold, you could write

varArgs(arrFold(mg => mx => chain(g => map(g) (mx)) (mg)) (of(f)))

but you need to notice that this builds a different call tree than the right fold:

of(f)
chain(of(f))(g0 => map(m0)(g0))
chain(chain(of(f))(g0 => map(m0)(g0)))(g1 => map(m1)(g1))
chain(chain(chain(of(f))(g0 => map(m0)(g0)))(g1 => map(m1)(g1)))(g2 => map(m2)(g2))

vs (with the continuations already applied)

of(f)
chain(m0)(x0 => of(f(x0)))
chain(m0)(x0 => chain(m1)(x1 => of(f(x0)(x1))))
chain(m0)(x0 => chain(m1)(x1 => chain(m2)(x2) => of(f(x0)(x1)(x2)))))

According to the monad laws, they should evaluate to the same, but in practice one might be more efficient than the other.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Clever - at least i think it is. I need to look deeper into this tomorrow. +1 because i am pretty sure i'll learn something tomorrow. –  Jun 02 '19 at 20:12
  • The `arrFold` function is a left fold. – Aadit M Shah Jun 03 '19 at 03:41
  • @Bergi _builds a different call tree_ - OK, this only matters if the binary operator isn't associative/commutative. Additionally, at least for `Array`s you can transform any left fold to a right one and vice versa by applying `flip`/`Array.prototype.reverse` to the operator and the consumed array respectively. This could cause performance iusses though... –  Jun 03 '19 at 09:02
0

You don't need the full power of monads for this particular use case. Applicative functors are all you need:

// type Cont r a = (a -> r) -> r

// type Async a = Cont (IO ()) a

// pure :: a -> Async a
const pure = a => k => k(a);

// ap :: Async (a -> b) -> Async a -> Async b
const ap = asyncF => asyncA => k => asyncF(f => asyncA(a => k(f(a))));

// delay :: (Number, a) -> Async a
const delay = (ms, a) => k => setTimeout(k, ms, a);

// async1, async2, async3, async4 :: Async Number
const async1 = delay(100, 1);
const async2 = delay(200, 2);
const async3 = delay(300, 3);
const async4 = delay(400, 4);

// sum :: Number -> Number -> Number -> Number -> Number
const sum = a => b => c => d => a + b + c + d;

// uncurry :: (a -> b -> c) -> (a, b) -> c
const uncurry = f => (a, b) => f(a)(b);

// result :: Async Number
const result = [async1, async2, async3, async4].reduce(uncurry(ap), pure(sum));

// main :: IO ()
result(console.log);
console.log("1 second later...");

If you want, you can define an applicative context function (i.e. apply) as follows:

const apply = (asyncF, ...asyncArgs) => asyncArgs.reduce(uncurry(ap), asyncF);

const result = apply(pure(sum), async1, async2, async3, async4);

If you curry this function then you can create a lift function:

const apply = asyncF => (...asyncArgs) => asyncArgs.reduce(uncurry(ap), asyncF);

const lift = f => apply(pure(f));

const asyncSum = lift(sum);

const result = asyncSum(async1, async2, async3, async4);

Notice that reduce is the equivalent to arrFold. Hence, lift is equivalent to varLiftM.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • I guess I got the hint: Don't get too complicated and maybe my `varArgs` approach is too complicated. I'll keep that in mind. Oh, and I know the difference between applicative (depends on the previous effect) and monad (may additionally depend on the value of the previous effect) . At work I try to use the former as often as possible. –  Jun 03 '19 at 16:35
  • Yes, it is indeed convoluted. The problem is that although JavaScript is not a good functional programming language, yet people try to write complex functional programs using it. JavaScript is more suitable for Pythonic code than Haskelline code. However, because it tries to borrow elements of myriad languages it ends up being neither as versatile as Python nor as elegant as Haskell. If you are really keen on becoming a better functional programmer, try writing a compiler for a functional programming language in JavaScript. https://en.m.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours – Aadit M Shah Jun 03 '19 at 17:11
  • A compiler is an excellent project to work on if you're keen on becoming a great programmer. Not only will you learn how a programming language is designed, but also you will experience how to write and maintain a non-trivial piece of software. The best goal to strive for is to build a self-hosting compiler (i.e. a compiler for a source language written in the same source language, which is able to compile itself to some target language). I love compiler construction. I was pursuing my PhD in it before I decided to join the industry. If you ask questions about it I'd be more than happy to help – Aadit M Shah Jun 03 '19 at 17:17
  • Thanks for the advice. Maybe you recall my runtime type validator project a year ago. I always wanted to take the next step but I've never found the time so far. –  Jun 03 '19 at 18:55
  • Runtime type validation is nice but it'll never be as powerful as compile time type checking/inference. You can only do so much at runtime. – Aadit M Shah Jun 04 '19 at 00:36
  • Or maybe you don't do it yourself (as an exercise), but just use purescript in production :-) – Bergi Jun 07 '19 at 16:32
  • @Bergi PureScript is awesome, but what I really want to implement are the [Futamura projections](https://stackoverflow.com/q/45992580/783743). There are three steps. First, build a self-hosting compiler. Second, implement some powerful optimizations such as [call-by-need supercompilation](https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-835.pdf). Third, and perhaps the most difficult, implement a specializer. I'm currently working on this project over weekends, but I don't get time to work on it as much as I'd like to. – Aadit M Shah Jun 07 '19 at 17:42