如何存储Monoidal List的功能链的数据?

时间:2018-07-12 03:55:46

标签: javascript arrays list math functional-programming

这是我在此之前提出的问题的高级主题:

How to store data of a functional chain?

简单的想法是

下面的一个简单功能:

const L = a => L;

表格

L
L(1)
L(1)(2)
...

这似乎形成了一个列表,但实际数据根本没有存储,因此,如果需要存储数据,例如[1,2],完成任务的最明智的做法是什么?

其中一个突出的想法来自@ user633183,我将其标记为可接受的答案(请参阅问题链接),而@MatíasFidemraizer也提供了curried函数的另一个版本。

就这样:

const L = a => {
  const m = list => x => !x
    ? list
    : m([...list, x]);
  return m([])(a);
}; 

const list1 = (L)(1)(2)(3); //lazy : no data evaluation here
const list2 = (L)(4)(5)(6);

console.log(list1()) // now evaluated by the tail ()
console.log(list2())  

我真正喜欢的是结果是懒惰的评估。

尽管给定的方法可以满足我的要求,但是此功能已经失去了外部结构,或者我必须提一下:

代数结构

 const L = a => L;

形成列表,从根本上给我们algebraic structureidentity element,可能还会与MonoidMagma一起出现。

左为右身份

最简单的Monoid和身份示例之一是JavaScript中的数字以及"Strings"[Array]

0 + a === a === a + 0
1 * a === a === a * 1

在字符串中,空的""是标识元素。

  "" + "Hello world" === "Hello world" === "Hello world" + ""

同去[Array]

同一个词进入L

(L)(a) === (a) === (a)(L)

const L = a => L;

const a = L(5); // number is wrapped or "lift" to Type:L
                // Similarity of String and Array
                // "5"  [5]

//left identity
console.log(
  (L)(a) === (a)    //true 
);
 
//right identity
console.log(
  (a) === (a)(L)    //true
); 

和明显的身份不变性:

const L = a => L;
 
console.log(
  (L)(L) === (L)    //true
); 
console.log(
  (L)(L)(L) === (L)    //true
); 
console.log(
  (L)(L)(L)(L) === (L)    //true
); 

还有以下内容:

const L = a => L;

const a = (L)(1)(2)(3);
const b = (L)(1)(L)(2)(3)(L);

 
console.log(
   (a) === (b)    //true 
);
 

问题

实现满足3个要求的Array.push的最聪明或最优雅的方法是什么(功能齐全且无变异(也没有L)?

要求0-身份

一个简单的功能:

const L = a => L;

已经满足了我们已经看到的身份法。

要求1-eval()方法

尽管L符合身份法,但是没有方法可以访问列出的/累积的数据。

(我上一个问题中提供的答案提供了数据累积能力,但违反了身份定律。)

惰性评估似乎是正确的方法,因此请提供更清晰的规范:

提供eval的{​​{1}}方法

L

要求3-Monoid联想法

除了突出的“识别”结构外,Monoids还满足Associative law

const L = a => L; // needs to enhance to satisfy the requirements

const a = (L)(1)(2)(3);
const b = (L)(1)(L)(2)(3)(L);


console.log(
   (a) === (b)    //true 
);

console.log(
   (a).eval()    //[1, 2, 3]
);

console.log(
   (b).eval()    //[1, 2, 3]
);

这仅表示“拉平列表”,换句话说,该结构不包含嵌套列表。

(a * b) * c === a * b * c === a * (b * c) 不好。

示例:

[a, [b, c]]

这就是将const L = a => L; // needs to enhance to satisfy the requirements const a = (L)(1)(2); const b = (L)(3)(4); const c = (L)(99); const ab = (a)(b); const bc = (b)(c); const abc1 = (ab)(c); const abc2 = (a)(bc); console.log( abc1 === abc2 // true for Associative ); console.log( (ab).eval() //[1, 2, 3, 4] ); console.log( (abc1).eval() //[1, 2, 3, 4, 99] ); console.log( (abc2).eval() //[1, 2, 3, 4, 99] ); 实现为类的3个要求。

这对我来说是函数编程的巨大挑战,实际上我自己尝试了一段时间,但问了前面的问题,分享我自己的挑战并聆听人们的声音并阅读他们的优雅代码是一种很好的做法。 / p>

谢谢。

2 个答案:

答案 0 :(得分:1)

镜像测试

要使L具有自我意识,我们必须以某种方式标记它创建的值。这是一个通用特征,我们可以使用一对函数对其进行编码。我们对行为设定了期望–

is (Foo, 1)            // false   1 is not a Foo
is (Foo, tag (Foo, 1)) // true    tag (Foo, 1) is a Foo

下面,我们实现istag。我们希望对它们进行设计,以便可以放入 any 值,并在以后可靠地确定该值的标签。我们为nullundefined设置例外。

const Tag =
  Symbol ()

const tag = (t, x) =>
  x == null
    ? x
    : Object.assign (x, { [Tag]: t })
    
const is = (t, x) =>
  x == null
    ? false
    : x[Tag] === t
  
const Foo = x =>
  tag (Foo, x)
  
console.log
  ( is (Foo, 1)         // false
  , is (Foo, [])        // false
  , is (Foo, {})        // false
  , is (Foo, x => x)    // false
  , is (Foo, true)      // false
  , is (Foo, undefined) // false
  , is (Foo, null)      // false
  )
  
console.log
  ( is (Foo, Foo (1))         // true    we can tag primitives
  , is (Foo, Foo ([]))        // true    we can tag arrays
  , is (Foo, Foo ({}))        // true    we can tag objects
  , is (Foo, Foo (x => x))    // true    we can even tag functions
  , is (Foo, Foo (true))      // true    and booleans too
  , is (Foo, Foo (undefined)) // false   but! we cannot tag undefined
  , is (Foo, Foo (null))      // false   or null
  )

我们现在有了一个函数Foo,该函数能够区分它产生的值。 Foo变得自我意识–

const Foo = x =>
  is (Foo, x)
    ? x              // x is already a Foo
    : tag (Foo, x)   // tag x as Foo

const f =
  Foo (1)

Foo (f) === f // true

L意识较高

使用istag,我们可以使List具有自我意识。如果输入了List标记的值,则List可以根据您的设计规范进行响应。

const None =
  Symbol ()

const L = init =>
{ const loop = (acc, x = None) =>
    // x is empty: return the internal array
    x === None
      ? acc

    // x is a List: concat the two internal arrays and loop
    : is (L, x)
      ? tag (L, y => loop (acc .concat (x ()), y))

    // x is a value: append and loop
    : tag (L, y => loop ([ ...acc, x ], y))

  return loop ([], init) 
}

我们使用您的测试数据进行尝试–

const a =
  L (1) (2)

const b =
  L (3) (4)

const c =
  L (99)

console.log
  ( (a) (b) (c) () // [ 1, 2, 3, 4, 99 ]
  , (a (b)) (c) () // [ 1, 2, 3, 4, 99 ]
  , (a) (b (c)) () // [ 1, 2, 3, 4, 99 ]
  )

值得将此实现与最后一个实现进行比较–

// previous implementation
const L = init =>
{ const loop = (acc, x) =>
    x === undefined   // don't use !x, read more below
      ? acc
      : y => loop ([...acc, x], y)
  return loop ([], init)
}

在我们的修订版中,为is (L, x)添加了一个新分支,该分支定义了新的Monoidal行为。最重要的是,所有返回的值都以tag (L, ...)包装,以便以后可以将其标识为带有标签的L。另一个变化是显式使用None符号。在此帖子的末尾添加了有关此内容的其他说明。

L个值的相等性

要确定L(x)L(y)的相等性,我们面临另一个问题。 JavaScript中的复合数据用对象表示,这些对象不能简单地与===运算符进行比较

console.log
  ( { a: 1 } === { a: 1 } ) // false

我们可以为L写一个相等函数,也许叫做Lequal

const l1 =
  L (1) (2) (3)

const l2 =
  L (1) (2) (3)

const l3 =
  L (0)

console.log
  ( Lequal (l1, l2) // true
  , Lequal (l1, l3) // false
  , Lequal (l2, l3) // false
  )

但是我不会在这篇文章中介绍如何做到这一点。如果您有兴趣,我会在this Q&A中介绍该主题。

// Hint:
const Lequal = (l1, l2) =>
  arrayEqual   // compare two arrays
    ( l1 ()    // get actual array of l1
    , l2 ()    // get actual array of l2
    )

深入标记

我在这里使用的标记技术是我在其他答案中使用的一种标记技术。它伴随着更广泛的示例here

其他评论

请勿使用!x测试空值,因为对于任何“虚假” true,它将返回x。例如,如果您要列出L (1) (0) (3) ...的列表,它将在1之后停止,因为!0true。伪造的值包括0""(空字符串),nullundefinedNaN,当然还有false本身。因此,我们使用显式None符号来更精确地标识列表何时终止。 所有其他输入都附加到内部数组。

也不要依靠JSON.stringify之类的黑客来测试对象的相等性。绝对需要结构遍历。

const x = { a: 1, b: 2 }
const y = { b: 2, a: 1 }

console.log
  (JSON.stringify (x) === JSON.stringify (y)) // false

console.log
  (Lequal (L (x), L (y))) // should be true!

有关如何解决此问题的建议,请参见this Q&A

答案 1 :(得分:1)

您的数据类型不一致!

因此,您要创建一个monoid。考虑一个monoid的结构:

class Monoid m where
    empty :: m           -- identity element
    (<*>) :: m -> m -> m -- binary operation

-- It satisfies the following laws:

empty <*> x = x = x <*> empty     -- identity law
(x <*> y) <*> z = x <*> (y <*> z) -- associativity law

现在,请考虑您的数据类型的结构:

(L)(a) = (a) = (a)(L)     // identity law
((a)(b))(c) = (a)((b)(c)) // associativity law

因此,根据您的说法,标识元素为L,二进制操作为function application。但是:

(L)(1)                  // This is supposed to be a valid expression.
(L)(1) != (1) != (1)(L) // But it breaks the identity law.

// (1)(L) is not even a valid expression. It throws an error. Therefore:

((L)(1))(L)                // This is supposed to be a valid expression.
((L)(1))(L) != (L)((1)(L)) // But it breaks the associativity law.

问题在于您正在将二进制操作与反向列表构造函数进行合并:

// First, you're using function application as a reverse cons (a.k.a. snoc):

// cons :: a -> [a] -> [a]
// snoc :: [a] -> a -> [a] -- arguments flipped

const xs = (L)(1)(2); // [1,2]
const ys = (L)(3)(4); // [3,4]

// Later, you're using function application as the binary operator (a.k.a. append):

// append :: [a] -> [a] -> [a]

const zs = (xs)(ys); // [1,2,3,4]

如果您将函数应用程序用作snoc,则也不能将其用于append

snoc   :: [a] ->  a  -> [a]
append :: [a] -> [a] -> [a]

请注意,类型不匹配,但是即使它们匹配,您仍然不希望一项操作执行两项操作。

您想要的是差异列表。

difference list是一个函数,它接受一个列表并在其前面添加另一个列表。例如:

const concat = xs => ys => xs.concat(ys); // This creates a difference list.

const f = concat([1,2,3]); // This is a difference list.

console.log(f([])); // You can get its value by applying it to the empty array.

console.log(f([4,5,6])); // You can also apply it to any other array.

关于差异列表的最酷的事情是它们形成了一个monoid,因为它们只是endofunctions

const id = x => x; // The identity element is just the id function.

const compose = (f, g) => x => f(g(x)); // The binary operation is composition.

compose(id, f) = f = compose(f, id);                   // identity law
compose(compose(f, g), h) = compose(f, compose(g, h)); // associativity law

更好的是,您可以将它们打包到一个整洁的小类中,其中函数组成是点运算符:

class DList {
    constructor(f) {
        this.f  = f;
        this.id = this;
    }

    cons(x) {
        return new DList(ys => this.f([x].concat(ys)));
    }

    concat(xs) {
        return new DList(ys => this.f(xs.concat(ys)));
    }

    apply(xs) {
        return this.f(xs);
    }
}

const id = new DList(x => x);

const cons = x => new DList(ys => [x].concat(ys));   // Construct DList from value.

const concat = xs => new DList(ys => xs.concat(ys)); // Construct DList from array.

id . concat([1, 2, 3]) = concat([1, 2, 3]) = concat([1, 2, 3]) . id // identity law

concat([1, 2]) . cons(3) = cons(1) . concat([2, 3]) // associativity law

您可以使用apply方法来检索DList的值,如下所示:

class DList {
    constructor(f) {
        this.f  = f;
        this.id = this;
    }

    cons(x) {
        return new DList(ys => this.f([x].concat(ys)));
    }

    concat(xs) {
        return new DList(ys => this.f(xs.concat(ys)));
    }

    apply(xs) {
        return this.f(xs);
    }
}

const id = new DList(x => x);

const cons = x => new DList(ys => [x].concat(ys));

const concat = xs => new DList(ys => xs.concat(ys));

const identityLeft  = id . concat([1, 2, 3]);
const identityRight = concat([1, 2, 3]) . id;

const associativityLeft  = concat([1, 2]) . cons(3);
const associativityRight = cons(1) . concat([2, 3]);

console.log(identityLeft.apply([]));  // [1,2,3]
console.log(identityRight.apply([])); // [1,2,3]

console.log(associativityLeft.apply([]));  // [1,2,3]
console.log(associativityRight.apply([])); // [1,2,3]

使用差异列表而不是常规列表(功能列表,而不是JavaScript数组)的优点在于,由于列表是从右到左连接的,因此连接效率更高。因此,如果要连接多个列表,它不会一遍又一遍地复制相同的值。