【问题标题】:Here is the C# Monad, where is the problem?这是C# Monad,问题出在哪里?
【发布时间】:2020-01-26 18:39:54
【问题描述】:

阅读Previous SO Question 我很困惑地发现Eric Lippert 说不能在C# 中为所有Monad 定义接口,使用如下实现:

typeInterface Monad<MonadType<A>>
{
       static MonadType<A> Return(A a);
       static MonadType<B> Bind<B>(MonadType<A> x, Func<A, MonadType<B>> f);
}

我的问题是问题中列出的所有问题似乎都有简单的解决方案:

  • 没有“高级类型” => 使用父接口
  • 接口中没有静态方法。 => 为什么要使用静态?!只使用实例方法

Monad 是一种允许在包装类型上链接操作的模式 为所有 Monad 定义 C# 接口似乎很容易,允许我们为所有 monad 编写泛型类 问题出在哪里?

using System;
using System.Linq;          
public class Program
{
    public static void Main()
    {//it works, where's the problem?
            new SequenceMonad<int>(5)
                .Bind(x => new SequenceMonad<float>(x + 7F))
                .Bind(x => new SequenceMonad<double>(x + 5D))
                ;
    }
    interface IMonad<T>{

        IMonad<T> Wrap(T a);
        IMonad<U> Bind<U>(Func<T, IMonad<U>> map);
        T UnWrap();//if we can wrap we should be able to unwrap
    }
    class GenericClassForAllMonads<T>
    {//example writing logic for all monads
        IMonad<U> DoStuff<U>(IMonad<T> input, Func<T, IMonad<U>> map)
        { return map(input.UnWrap()); }
    }
    class SequenceMonad<T> : IMonad<T> where T:new()
    {//specific monad implementation
        readonly T[] items;//immutable
        public SequenceMonad(T a)
        {
            Console.WriteLine("wrapped:"+a);
            items =  new[] { a }; 
        }
        public IMonad<B> Bind<B>(Func<T, IMonad<B>> map)
        {  return map(UnWrap()); }

        public T UnWrap()
        { return items == null? default(T) : items.FirstOrDefault();  }

        public IMonad<T> Wrap(T a)
        {
            Console.WriteLine("wrapped:"+a);
            return new SequenceMonad<T>(a); 
        }
    }
}

【问题讨论】:

  • 所以为了在 Monad 中包装一个值,你首先必须创建一个?这真的有意义吗?
  • 现在的问题是接口不能保证类有一个构造函数会包装这个值。 Eric 从未说过你不能用 C# 编写 monad,只是说你不能创建一个接口来保证实现它的类满足要求。
  • 更准确地说,要求是传入一个值,然后得到一个包装该值的 monad。这可以是构造函数或静态方法,但都不能在接口中定义。剩下的就是实例方法,它们要么创建一个新的 monad,要么改变一个现有的 monad。但是要理解,从功能意义上讲,它接受一个 monad 和一个值并返回一个包装该值的 monad(但要求只传入该值)。
  • “如果我们可以包装,那么我们应该能够打开”不是单子法则。 “我们可以展开”是 comonad 法则,而“经常有展开”是关于常用 monad 的事实,而不是要求。
  • 我注意到你的序列单子没有实现序列单子。序列单子的特征行为是绑定操作的行为是映射和连接的。也就是说,如果我们有一个整数序列s 等于{1, 2, 3},那么s.Bind(x =&gt; Repeat(x, 2)) 就是序列{1,1,2,2,3,3},例如。您的实现中没有任何串联逻辑。

标签: c# generics interface monads


【解决方案1】:

为所有 monad 定义 C# 接口似乎很容易。问题出在哪里?

您的建议是:

interface IMonad<T>
{
    IMonad<T> Wrap(T a);
    IMonad<U> Bind<U>(Func<T, IMonad<U>> map);
}

我省略了“展开”,因为提取操作的存在不是 monad 的要求。 (很多 monad 都有这个操作,但不是所有的都有。如果你 require 一个提取操作,你可能实际上是在使用一个 comonad。)

你问为什么这是错误的。这在几个方面是错误的。

第一种方法是错误的:如果没有实例,就无法通过Wrap 创建 monad 的新实例!你有一个先有鸡还是先有蛋的问题。

“wrap”、“unit”或“return”操作——不管你怎么称呼它——在逻辑上都是一个静态工厂;这是 你如何创建 monad 的新实例。这不是对实例的操作。这是对类型的静态方法的要求。 (或者,要求一个类型实现一个特定的构造函数,这实际上是同一件事。无论哪种方式,目前 C# 都不支持。)

让我们在下一点将Wrap 排除在外。为什么Bind错了?

第二种错误的方式是您没有正确的限制。您的界面说 T 的单子是提供返回 U 单子的绑定操作的东西。但这还不够严格!假设我们有一个单子Maybe&lt;T&gt; : IMonad&lt;T&gt;。现在假设我们有这个实现:

class Wrong<T> : IMonad<T>
{
  public IMonad<U> Bind<U>(Func<T, IMonad<U>> map)
  {
    return new Maybe<U>();
  }
}

满足契约,它告诉我们契约不是真正的 monad 契约。 monad 契约应该是 Wrong&lt;T&gt;.Bind&lt;U&gt; 返回 Wrong&lt;U&gt;,而不是 IMonad&lt;U&gt;!但是我们无法在 C# 中表达“bind 返回定义 bind 的类的实例”。

同样是错误的,因为调用者提供的Func必须返回Wrong&lt;U&gt;,而不是IMonad&lt;U&gt;。假设我们有第三个单子,比如State&lt;T&gt;。我们本来可以

Wrong<Frog> w = whatever;
var result = w.Bind<Newspaper>(t=>new State<Newspaper>());

现在这一切都搞砸了。 Wrong&lt;T&gt;.Bind&lt;U&gt; 必须接受一个返回一些 Wrong&lt;U&gt; 的函数,并且它本身必须返回相同类型的 Wrong&lt;U&gt;,但是这个接口允许我们有一个绑定,它接受一个返回 State&lt;Newspaper&gt; 但绑定返回 Maybe&lt;Newspaper&gt; 的函数.这完全违反了单子模式。您尚未在界面中捕获 monad 模式。

C# 类型系统不足以表达约束“当方法被实现时,它必须返回执行实现的类的实例”。如果 C# 有一个“this_type”编译时注解,那么 Bind 可以表示为一个接口,但 C# 没有那个注解。

【讨论】:

  • 哇!不敢相信你自己回答了我:) 谢谢!现在我终于明白了。非常感谢您花时间详细解释。
  • @EricLippert 接口默认实现的 C# 提案规范是否完全改变了这一点?我知道规范的一部分是在接口上包含静态成员。 Draft Proposal“完全改变”我的具体意思是,通过在 IMonad 类型本身上提供静态工厂方法,它是否解决了您所描述的先有鸡还是先有蛋的问题?
  • @JustinBlakley:它解决了如何制作一个说“这种类型有一个特定的静态成员”的合同的问题,但它没有解决答案最后一段中的问题,并且这是棘手的一点。我们真正需要表示monad模式的是“如果类型C&lt;T&gt;实现了契约M&lt;T&gt;然后C&lt;T&gt;有一个方法接受Func&lt;T, C&lt;U&gt;&gt;并返回C&lt;U&gt;&gt;我们没办法这么说。
  • 显然他们正在朝这个方向努力。值得一提的是,这将随着 C# 9.0 中协变返回的引入而改变,因此返回更具体的类型(Wrong 而不是 IMonad)将是合法的。
  • @HarunCerim:我不太确定你在这里得到什么;返回类型协方差在这里有什么帮助? monad 高级类型描述了类型系统必须施加的限制,才能使类型成为合法的 monad;相比之下,返回类型协方差删除类型系统的限制。你能进一步解释一下吗,因为我认为我遗漏了一些东西。
猜你喜欢
  • 1970-01-01
  • 2011-11-04
  • 1970-01-01
  • 2013-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-21
相关资源
最近更新 更多