Extracting data from a function chain without arrays

后端 未结 5 920
臣服心动
臣服心动 2021-01-14 01:36

This is an advanced topic of

How to store data of a functional chain of Monoidal List?

I am pretty sure we can somehow extract data from a function chain wit

相关标签:
5条回答
  • 2021-01-14 01:48

    I've gone through the various questions you have but I'm still not sure I entirely understand what you're looking for. On the off chance you're simply looking to represent a linked list, here is a "dumb" representation that does not use clever tricks like overloaded arguments or default parameter values:

    const List = (() => {
      const nil = Symbol()
    
      // ADT
      const Nil = nil
      const Cons = x => xs => ({ x, xs })
      const match = ({ Nil, Cons }) => l => l === nil ? Nil : Cons(l.x)(l.xs)
    
      // Functor
      const map = f => match({
        Nil,
        Cons: x => xs => Cons(f(x))(map(f)(xs))
      })
    
      // Foldable
      const foldr = f => z => match({
        Nil: z,
        Cons: x => xs => f(x)(foldr(f)(z)(xs)) // danger of stack overflow!
                                               // https://wiki.haskell.org/Foldr_Foldl_Foldl%27
      })
    
      return { Nil, Cons, match, map, foldr }
    })()
    
    const { Nil, Cons, match, map, foldr } = List
    const toArray = foldr(x => xs => [x, ...xs])([])
    
    const l = Cons(1)(Cons(2)(Cons(3)(Nil)))
    
    const l2 = map(x => x * 2)(l)
    const l3 = map(x => x * 3)(l2)
    
    const a = toArray(l3)
    console.log(a) // => [6, 12, 18]

    0 讨论(0)
  • 2021-01-14 01:51

    Work in progress

    Thanks to the stunning contribution by @user3297291 , I somehow could refactor the code to fit my specification, but not working because I am lost the concept during the implementation :(

    The point is whole thing must be curried, and no object.method is involved.

    Can anyone "debug" please :)

    The initial value is set to the first element, in this example as 1

    I think this is almost done.

    const isFunction = f => (typeof f === 'function');
    
    const Empty = Symbol();
    
    const L = (x = Empty) => (y = Empty) => z => isFunction(z)
        ? (() => {
            const fold = f => seed => f(x)(y) === Empty
                ? seed
                : (L)(y)(f);
            return fold(z)(x);
        })()
        : L(z)(L(x)(y));
    
    
    const sum = a => b => a + b;
    
    
    console.log(
        L(1)(2)(3)(4)(5)(6)(sum)
    );

    Output

     z => isFunction(z)
        ? (() => {
            const fold = f => seed => f(x)(y) === Empty
                ? seed
                : (L)(y)(f);
            return fold(z)(x);
        })()
        : L(z)(L(x)(y))
    
    0 讨论(0)
  • 2021-01-14 01:57

    I'll have to admit I haven't read through your linked questions and I'm mainly here for the fun puzzle... but does this help in any way?

    I figured you want to differentiate between adding an element (calling with a new value) and running a function on the list (calling with a function). Since I had to somehow pass the function to run, I couldn't get the (1) vs () syntax to work.

    This uses an interface that returns an object with concat to extend the list, and fold to run a reducer on the list. Again, not sure if it's a complete answer, but it might help you explore other directions.

    const Empty = Symbol();
    
    const L = (x, y = Empty) => ({
      concat: z => L(z, L(x, y)),
      fold: (f, seed) => f(x, y === Empty ? seed : y.fold(f, seed))
    });
    
    const sum = (a, b) => a + b;
    
    
    console.log(
      L(1)
        .concat(2).concat(3).concat(4).concat(5).concat(6)
        .fold(sum, 0)
    )

    0 讨论(0)
  • 2021-01-14 02:05

    Quite the series of questions you have here. Here's my take on it:

    We start with a way to construct lists

    • nil is a constant which represents the empty list
    • cons (x, list) constructs a new list with x added to the front of list

    // nil : List a
    const nil =
      (c, n) => n
    
    // cons : (a, List a) -> List a
    const cons = (x, y = nil) =>
      (c, n) => c (y (c, n), x)
    
    // list : List Number
    const myList = 
      cons (1, cons (2, cons (3, cons (4, nil))))
    
    console.log (myList ((x, y) => x + y, 0))
    // 10

    And to satisfy your golfy variadic curried interface, here is autoCons

    const autoCons = (init, n) => 
    { const loop = acc => (x, n) =>
        isFunction (x)
          ? acc (x, n)
          : loop (cons (x, acc))
      return loop (nil) (init, n)
    }
    
    const isFunction = f =>
      f != null && f.constructor === Function && f.length === 2
    
    const nil =
      (c, n) => n
    
    const cons = (x, y = nil) =>
      (c, n) => c (y (c, n), x)
    
    console.log
      ( autoCons (1) ((x,y) => x + y, 0)             // 1
      , autoCons (1) (2) ((x,y) => x + y, 0)         // 3
      , autoCons (1) (2) (3) ((x,y) => x + y, 0)     // 6
      , autoCons (1) (2) (3) (4) ((x,y) => x + y, 0) // 10
      )

    Our encoding makes it possible to write other generic list functions, like isNil

    // isNil : List a -> Bool
    const isNil = l =>
      l ((acc, _) => false, true)
    
    console.log
      ( isNil (autoCons (1))     // false
      , isNil (autoCons (1) (2)) // false
      , isNil (nil)              // true
      )
    

    Or like length

    // length : List a -> Int
    const length = l =>
      l ((acc, _) => acc + 1, 0)
    
    console.log
      ( length (nil)                  // 0
      , length (autoCons (1))         // 1
      , length (autoCons (1) (2))     // 2
      , length (autoCons (1) (2) (3)) // 3
      )
    

    Or nth which fetches the nth item in the list

    // nth : Int -> List a -> a
    const nth = n => l =>
      l ( ([ i, res ], x) =>
            i === n
              ? [ i + 1, x ]
              : [ i + 1, res]
        , [ 0, undefined ]
        ) [1]
    
    console.log
      ( nth (0) (autoCons ("A") ("B") ("C")) // "A"
      , nth (1) (autoCons ("A") ("B") ("C")) // "B"
      , nth (2) (autoCons ("A") ("B") ("C")) // "C"
      , nth (3) (autoCons ("A") ("B") ("C")) // undefined
      )
    

    We can implement functions like map and filter for our list

    // map : (a -> b) -> List a -> List b
    const map = f => l =>
      l ( (acc, x) => cons (f (x), acc)
        , nil
        )
    
    // filter : (a -> Bool) -> List a -> List a
    const filter = f => l =>
      l ( (acc, x) => f (x) ? cons (x, acc) : acc
        , nil
        )
    

    We can even make a program using our list which takes a list as an argument

    // rcomp : (a -> b) -> (b -> c) -> a -> c
    const rcomp = (f, g) =>
      x => g (f (x))
    
    // main : List String -> String   
    const main = letters =>
      autoCons (map (x => x + x))
               (filter (x => x !== "dd"))
               (map (x => x.toUpperCase()))
               (rcomp, x => x)
               (letters)
               ((x, y) => x + y, "")
    
    main (autoCons ("a") ("b") ("c") ("d") ("e"))
    // AABBCCEE
    

    Run the program in your browser below

    const nil =
      (c, n) => n
    
    const cons = (x, y = nil) =>
      (c, n) => c (y (c, n), x)
    
    const isFunction = f =>
      f != null && f.constructor === Function && f.length === 2
    
    const autoCons = (init, n) => 
    { const loop = acc => (x, n) =>
        isFunction (x)
          ? acc (x, n)
          : loop (cons (x, acc))
      return loop (nil) (init, n)
    }
    
    const map = f => l =>
      l ( (acc, x) => cons (f (x), acc)
        , nil
        )
    
    const filter = f => l =>
      l ( (acc, x) => f (x) ? cons (x, acc) : acc
        , nil
        )
    
    const rcomp = (f, g) =>
      x => g (f (x))
    
    const main = letters =>
      autoCons (map (x => x + x))
               (filter (x => x !== "dd"))
               (map (x => x.toUpperCase()))
               (rcomp, x => x)
               (letters)
               ((x, y) => x + y, "")
    
    console.log (main (autoCons ("a") ("b") ("c") ("d") ("e")))
    // AABBCCEE


    Sorry, my bad

    Let's rewind and look at our initial List example

    // list : List Number
    const myList = 
      cons (1, cons (2, cons (3, cons (4, nil))))
    
    console.log
      ( myList ((x, y) => x + y, 0) // 10
      )
    

    We conceptualize myList as a list of numbers, but we contradict ourselves by calling myList (...) like a function.

    This is my fault. In trying to simplify the example, I crossed the barrier of abstraction. Let's look at the types of nil and cons

    // nil : List a
    // cons : (a, List a) -> List a
    

    Given a list of type List a, how do we get a value of type a out? In the example above (repeated below) we cheat by calling myList as a function. This is internal knowledge that only the implementer of nil and cons should know

    // myList is a list, not a function... this is confusing...
    console.log
      ( myList ((x, y) => x + y, 0) // 10
      )
    

    If you look back at our original implementation of List,

    // nil : List a
    const nil =
      (c, n) => n
    
    // cons : (a, List a) -> List a
    const cons = (x, y = nil) =>
      (c, n) => c (y (c, n), x)
    

    I also cheated you giving simplified type annotations like List a. What is List, exactly?

    We're going to address all of this and it starts with our implementation of List

    List, take 2

    Below nil and cons have the exact same implementation. I've only fixed the type annotations. Most importantly, I added reduce which provides a way to get values "out" of our list container.

    The type annotation for List is updated to List a r – this can be understood as "a list containing values of type a that when reduced, will produce a value of type r."

    // type List a r = (r, a) -> r
    
    // nil : List a r
    const nil =
      (c, n) => n
    
    // cons : (a, List a r) -> List a r
    const cons = (x, y = nil) =>
      (c, n) => c (y (c, n), x)
    
    // reduce : ((r, a) -> r, r) -> List a -> r
    const reduce = (f, init) => l =>
      l (f, init)
    

    Now we can maintain List as a sane type, and push all the wonky behavior you want into the autoCons function. Below we update autoCons to work with our list acc using our new reduce function

    const autoCons = (init, n) => 
    { const loop = acc => (x, n) =>
        isFunction (x)
          // don't break the abstraction barrier
          ? acc (x, n)
          // extract the value using our appropriate list module function
          ? reduce (x, n) (acc)
          : loop (cons (x, acc))
      return loop (nil) (init, n)
    }

    So speaking of types, let's examine the type of autoCons

    autoCons (1)                  // "lambda (x,n) => isFunction (x) ...
    autoCons (1) (2)              // "lambda (x,n) => isFunction (x) ...
    autoCons (1) (2) (3)          // "lambda (x,n) => isFunction (x) ...
    autoCons (1) (2) (3) (add, 0) // 6
    

    Well autoCons always returns a lambda, but that lambda has a type that we cannot determine – sometimes it returns another lambda of its same kind, other times it returns a completely different result; in this case some number, 6

    Because of this, we cannot easily mix and combine autoCons expressions with other parts of our program. If you drop this perverse drive to create variadic curried interfaces, you can make an autoCons that is type-able

    // autoCons : (...a) -> List a r
    const autoCons = (...xs) =>
    { const loop = (acc, x = nil, ...xs) =>
        x === nil
          ? acc
          : loop (cons (x, acc), ...xs)
      return loop (nil, ...xs)
    }
    

    Because autoCons now returns a known List (instead of the mystery unknown type caused by variadic currying), we can plug an autoCons list into the various other functions provided by our List module.

    const c =
      autoCons (1, 2, 3)
    
    const d =
      autoCons (4, 5, 6)
    
    console.log
      ( toArray (c)                         // [ 1, 2, 3 ]
      , toArray (map (x => x * x) (d))      // [ 16, 25, 36 ]
      , toArray (filter (x => x != 5) (d))  // [ 4, 6 ]
      , toArray (append (c, d))             // [ 1, 2, 3, 4, 5, 6 ]
      )
    

    These kind of mix-and-combine expressions is not possible when autoCons returns a type we cannot rely upon. Another important thing to notice is the List module gives us a place to expand its functionality. We can easily add functions used above like map, filter, append, and toArray – you lose this flexibility when trying to shove everything through the variadic curried interface

    Let's look at those additions to the List module now – as you can see, each function is well-typed and has behavior we can rely upon

    // type List a r = (r, a) -> r
    
    // nil : List a r
    // cons : (a, List a r) -> List a r
    // reduce : ((r, a) -> r, r) -> List a r -> r
    
    // length : List a r -> Int
    const length =
      reduce
        ( (acc, _) => acc + 1
        , 0
        )
    
    // map : (a -> b) -> List a r -> List b r
    const map = f =>
      reduce
        ( (acc, x) => cons (f (x), acc)
        , nil
        )
    
    // filter : (a -> Bool) -> List a r -> List a r
    const filter = f =>
      reduce
        ( (acc,x) =>  f (x) ? cons (x, acc) : acc
        , nil
        )
    
    // append : (List a r, List a r) -> List a r
    const append = (l1, l2) =>
      (c, n) =>
        l2 (c, l1 (c, n))
    
    // toArray : List a r -> Array a
    const toArray =
      reduce
        ( (acc, x) => [ ...acc, x ]
        , []
        )
    

    Even autoCons makes sense as part of our module now

    // autoCons : (...a) -> List a r
    const autoCons = (...xs) =>
    { const loop = (acc, x = nil, ...xs) =>
        x === nil
          ? acc
          : loop (cons (x, acc), ...xs)
      return loop (nil, ...xs)
    }
    

    Add any other functions to the List module

    // nth: Int -> List a r -> a
    // isNil : List a r -> Bool
    // first : List a r -> a
    // rest : List a r -> List a r
    // ...
    
    0 讨论(0)
  • 2021-01-14 02:06

    Given an expression like A(a)(b)(f) where f is a function, it's impossible to know whether f is supposed to be added to the list or whether it's the reducing function. Hence, I'm going to describe how to write expressions like A(a)(b)(f, x) which is equivalent to [a, b].reduce(f, x). This allows us to distinguish when the list ends depending upon how many arguments you provide:

    const L = g => function (x, a) {
        switch (arguments.length) {
        case 1: return L(k => g((f, a) => k(f, f(a, x))));
        case 2: return g((f, a) => a)(x, a);
        }
    };
    
    const A = L(x => x);
    
    const xs = A(1)(2)(3)(4)(5);
    
    console.log(xs((x, y) => x + y, 0));        // 15
    console.log(xs((x, y) => x * y, 1));        // 120
    console.log(xs((a, x) => a.concat(x), [])); // [1,2,3,4,5]

    It works due to continuations. Every time we add a new element, we accumulate a CPS function. Each CPS function calls the previous CPS function, thereby creating a CPS function chain. When we give this CPS function chain a base function, it unrolls the chain and allows us to reduce it. It's the same idea behind transducers and lenses.


    Edit: user633183's solution is brilliant. It uses the Church encoding of lists using right folds to alleviate the need for continuations, resulting in simpler code which is easy to understand. Here's her solution, modified to make foldr seem like foldl:

    const L = g => function (x, a) {
        switch (arguments.length) {
        case 1: return L((f, a) => f(g(f, a), x));
        case 2: return g(x, a);
        }
    };
    
    const A = L((f, a) => a);
    
    const xs = A(1)(2)(3)(4)(5);
    
    console.log(xs((x, y) => x + y, 0));        // 15
    console.log(xs((x, y) => x * y, 1));        // 120
    console.log(xs((a, x) => a.concat(x), [])); // [1,2,3,4,5]

    Here g is the Church encoded list accumulated so far. Initially, it's the empty list. Calling g folds it from the right. However, we also build the list from the right. Hence, it seems like we're building the list and folding it from the left because of the way we write it.


    If all these functions are confusing you, what user633183 is really doing is:

    const L = g => function (x, a) {
        switch (arguments.length) {
        case 1: return L([x].concat(g));
        case 2: return g.reduceRight(x, a);
        }
    };
    
    const A = L([]);
    
    const xs = A(1)(2)(3)(4)(5);
    
    console.log(xs((x, y) => x + y, 0));        // 15
    console.log(xs((x, y) => x * y, 1));        // 120
    console.log(xs((a, x) => a.concat(x), [])); // [1,2,3,4,5]

    As you can see, she is building the list backwards and then using reduceRight to fold the backwards list backwards. Hence, it looks like you're building and folding the list forwards.

    0 讨论(0)
提交回复
热议问题