Extracting data from a function chain without arrays

后端 未结 5 924
臣服心动
臣服心动 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 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
    // ...
    

提交回复
热议问题