【问题标题】:Implementing the Choice Type in C#在 C# 中实现选择类型
【发布时间】:2015-02-12 15:58:21
【问题描述】:

出于教育原因,我尝试在 C# 中实现 F# 中的选择和选项类型。这是受《真实世界函数式编程》一书和一些博客文章的启发,例如:http://bugsquash.blogspot.de/2011/08/refactoring-to-monadic-c-applicative.htmlhttp://tomasp.net/blog/idioms-in-linq.aspx/

我想让它工作,但我不知道如何实现选择类型的扩展(Bind、Map、SelectMany、...):

public static void Division()
{
    Console.WriteLine("Enter two (floating point) numbers:");

    (
        from f1 in ReadDouble().ToChoice("Could not parse input to a double.")
        from f2 in ReadDouble().ToChoice("Could not parse input to a double.")
        from result in Divide(f1, f2).ToChoice("Cannot divide by zero.")
        select result
    )
        .Match(
            x => Console.WriteLine("Result = {0}", x),
            x => Console.WriteLine("Error: {0}", x));
}

public static Option<double> Divide(double a, double b)
{
    return b == 0 ? Option.None<double>() : Option.Some(a / b);
}

public static Option<Double> ReadDouble()
{
    double i;
    if (Double.TryParse(Console.ReadLine(), out i))
        return Option.Some(i);
    else
        return Option.None<double>();
}

    public static Option<int> ReadInt()
    {
        int i;
        if (Int32.TryParse(Console.ReadLine(), out i))
            return Option.Some(i);
        else
            return Option.None<int>();
    }
}

选项类型如下所示:

public enum OptionType
{
    Some, None
}

public abstract class Option<T>
{
    private readonly OptionType _tag;

    protected Option(OptionType tag)
    {
        _tag = tag;
    }

    public OptionType Tag { get { return _tag; } }

    internal bool MatchNone()
    {
        return Tag == OptionType.None;
    }

    internal bool MatchSome(out T value)
    {
        value = Tag == OptionType.Some ? ((Some<T>)this).Value : default(T);
        return Tag == OptionType.Some;
    }

    public void Match(Action<T> onSome, Action onNone)
    {
        if (Tag == OptionType.Some)
            onSome(((Some<T>)this).Value);
        else
            onNone();
    }

    public Choice<T, T2> ToChoice<T2>(T2 value)
    {
        if (Tag == OptionType.Some)
        {
            T some;
            MatchSome(out some);
            return Choice.NewChoice1Of2<T, T2>(some);
        }
        else
            return Choice.NewChoice2Of2<T, T2>(value);
    }
}

internal class None<T> : Option<T>
{
    public None() : base(OptionType.None) { }
}

internal class Some<T> : Option<T>
{
    public Some(T value)
        : base(OptionType.Some)
    {
        _value = value;
    }
    private readonly T _value;
    public T Value { get { return _value; } }
}

public static class Option
{
    public static Option<T> None<T>()
    {
        return new None<T>();
    }
    public static Option<T> Some<T>(T value)
    {
        return new Some<T>(value);
    }
}

public static class OptionExtensions
{
    public static Option<TResult> Map<T, TResult>(this Option<T> source, Func<T, TResult> selector)
    {
        T value;
        return source.MatchSome(out value) ? Option.Some(selector(value)) : Option.None<TResult>();
    }

    public static Option<TResult> Bind<T, TResult>(this Option<T> source, Func<T, Option<TResult>> selector)
    {
        T value;
        return source.MatchSome(out value) ? selector(value) : Option.None<TResult>();
    }

    public static Option<TResult> Select<T, TResult>(this Option<T> source, Func<T, TResult> selector)
    {
        return source.Map(selector);
    }

    public static Option<TResult> SelectMany<TSource, TValue, TResult>(this Option<TSource> source, Func<TSource, Option<TValue>> valueSelector, Func<TSource, TValue, TResult> resultSelector)
    {
        return source.Bind(s => valueSelector(s).Map(v => resultSelector(s, v)));
    }
}

这里是选择类型的实现:

public enum ChoiceType { Choice1Of2, Choice2Of2 };

public abstract class Choice<T1, T2>
{
    private readonly ChoiceType _tag;

    protected Choice(ChoiceType tag)
    {
        _tag = tag;
    }

    public ChoiceType Tag { get { return _tag; } }

    internal bool MatchChoice1Of2(out T1 value)
    {
        value = Tag == ChoiceType.Choice1Of2 ? ((Choice1Of2<T1, T2>)this).Value : default(T1);
        return Tag == ChoiceType.Choice1Of2;
    }

    internal bool MatchChoice2Of2(out T2 value)
    {
        value = Tag == ChoiceType.Choice2Of2 ? ((Choice2Of2<T1, T2>)this).Value : default(T2);
        return Tag == ChoiceType.Choice2Of2;
    }

    public void Match(Action<T1> onChoice1Of2, Action<T2> onChoice2Of2)
    {
        if (Tag == ChoiceType.Choice1Of2)
            onChoice1Of2(((Choice1Of2<T1, T2>)this).Value);
        else
            onChoice2Of2(((Choice2Of2<T1, T2>)this).Value);
    }
}

internal class Choice1Of2<T1, T2> : Choice<T1, T2>
{
    public Choice1Of2(T1 value)
        : base(ChoiceType.Choice1Of2)
    {
        _value = value;
    }
    private readonly T1 _value;

    public T1 Value { get { return _value; } }
}

internal class Choice2Of2<T1, T2> : Choice<T1, T2>
{
    public Choice2Of2(T2 value)
        : base(ChoiceType.Choice2Of2)
    {
        _value = value;
    }
    private readonly T2 _value;

    public T2 Value { get { return _value; } }
}

public static class Choice
{
    public static Choice<T1, T2> NewChoice1Of2<T1, T2>(T1 value)
    {
        return new Choice1Of2<T1, T2>(value);
    }

    public static Choice<T1, T2> NewChoice2Of2<T1, T2>(T2 value)
    {
        return new Choice2Of2<T1, T2>(value);
    }
}

编辑:

它实际上适用于下面的扩展。我真正不喜欢的是这个实现向 Choice 类型添加了特定于上下文的行为。这是因为 Choice1Of2 是首选,因为所有扩展方法都主要在它上操作,而不是在 Choice2Of2 上或两者兼而有之。 (但这就是消费代码的实际含义,所以我想这是让它工作的唯一方法。)

public static Choice<TResult, T2> Map<T1, T2, TResult>(this Choice<T1, T2> source, Func<T1, TResult> selector)
{
    T1 value1;
    if(source.MatchChoice1Of2(out value1))
    {
        return Choice.NewChoice1Of2<TResult, T2>(selector(value1));
    }

    T2 value2;
    if (source.MatchChoice2Of2(out value2))
    {
        return Choice.NewChoice2Of2<TResult, T2>(value2);
    }

    throw new InvalidOperationException("source (:Choice) has no value.");
}

public static Choice<TResult, T2> Bind<T1, T2, TResult>(this Choice<T1, T2> source, Func<T1, Choice<TResult, T2>> selector)
{
    T1 value1;
    if (source.MatchChoice1Of2(out value1))
    {
        return selector(value1);
    }

    T2 value2;
    if (source.MatchChoice2Of2(out value2))
    {
        return Choice.NewChoice2Of2<TResult, T2>(value2);
    }

    throw new InvalidOperationException("source (:Choice) has no value.");
}

public static Choice<TResult, T2> Select<T1, T2, TResult>(this Choice<T1, T2> source, Func<T1, TResult> selector)
{
    return source.Map(selector);
}

public static Choice<TResult, T2> SelectMany<TSource, TValue, T2, TResult>(this Choice<TSource, T2> source, Func<TSource, Choice<TValue, T2>> valueSelector, Func<TSource, TValue, TResult> resultSelector)
{
    return source.Bind(s => valueSelector(s).Map(v => resultSelector(s, v)));
}

【问题讨论】:

  • 你有什么问题?
  • 为了使第一个示例中的代码能够正常工作,编译器正在寻找选择类型中 Select 和 SelectMany 的实现。我不知道如何实现它们。在选项类型中它工作得很好,但在选择中你不需要两个选择器函数来选择 T1 和 T2 类型。这将如何运作?
  • 嗨@mauricio,谢谢。但是我错过了我特别感兴趣的 Select 和 SelectMany 的实现。
  • @user1993065 它们作为扩展方法编写起来很简单。

标签: c# linq f# functional-programming monads


【解决方案1】:

由于Choice有两个类型参数,你需要修复第一个才能写SelectSelectMany(绑定):

public abstract class Choice<T1, T2>
{
    public abstract Choice<T1, T3> Select<T3>(Func<T2, T3> f);
    public abstract Choice<T1, T3> SelectMany<T3>(Func<T2, Choice<T1, T3>> f);
}

Choice1Of2 的实现很简单:

public override Choice<T1, T3> Select<T3>(Func<T2, T3> f)
{
    return new Choice1Of2<T1, T3>(this._value);
}

public override Choice<T1, T3> SelectMany<T3>(Func<T2, Choice<T1, T3>> f)
{
    return new Choice1Of2<T1, T3>(this._value);
}

对于Choice2Of2,您只需为给定函数提供内部值:

public override Choice<T1, T3> Select<T3>(Func<T2, T3> f)
{
    return new Choice2Of2<T1, T3>(f(this.Value));
}
public override Choice<T1, T3> SelectMany<T3>(Func<T2, Choice<T1, T3>> f)
{
    return f(this._value);
}

您可能还需要一个 BiSelect 函数来映射两个类型参数:

public abstract BiSelect<T3, T4>(Func<T1, T3> ff, Func<T2, T4> fs);

如果您想将SelectMany 与 linq 查询语法一起使用,您需要实现另一个重载,如下所示:

public abstract Choice<T1, T4> SelectMany<T3, T4>(Func<T2, Choice<T1, T3>> f, Func<T2, T3, T4> selector);

Choice1Of2 的实现与之前类似:

public override Choice<T1, T4> SelectMany<T3, T4>(Func<T2, Choice<T1, T3>> f, Func<T2, T3, T4> selector)
{
    return new Choice1Of2<T1, T4>(this._value);
}

Choice2Of2 的实现是:

public override Choice<T1, T4> SelectMany<T3, T4>(Func<T2, Choice<T1, T3>> f, Func<T2, T3, T4> selector)
{
    T2 val = this._value;
    var e = f(val);
    return e.Select(v => selector(val, v));
}

你可以这样做:

var choice = from x in new Choice2Of2<string, int>(1)
             from y in new Choice2Of2<string, int>(4)
             select x + y;

【讨论】:

  • 这让我走上了正轨。虽然,这里发布的这段代码似乎不起作用。
  • 在这一行 from v1 in ReadDouble().ToChoice("Could not parse input.") 我收到此错误:Could not find an implementation of the query pattern for source type 'Choice&lt;double,string&gt;'. 'SelectMany' not found. - 我在原始帖子中发布了一个可行的解决方案。可能是 SelectMany 函数的参数不正确。
  • @user1993065 - 查询语法需要SelectMany 的另一个重载,请参阅更新。
  • 是的,这应该可以。其实我也是这么想的。除了我将这些功能实现为扩展并将选择器功能应用于 Choice1Of2 而不是 Choice2Of2。
【解决方案2】:

这是一个新的扩展类,可以让 'Match' 方法在 IEnumerable 上工作

public static class ChoiceExtensions
{

    // You need this method, because code 'select result' is a LINQ expression and it returns IEnumerable
    public static void Match<T1, T2>(this IEnumerable<Choice<T1, T2>> seq, Action<T1> onChoice1Of2, Action<T2> onChoice2Of2)
    {
        foreach (var choice in seq)
        {
            choice.Match(onChoice1Of2, onChoice2Of2);
        }
    }

    // This method will help with the complex matching
    public static Choice<T1, T2> Flat<T1, T2>(this Choice<Choice<T1, T2>, T2> choice)
    {
        Choice<T1, T2> result = null;

        choice.Match(
            t1 => result = t1,
            t2 => result = new Choice2Of2<T1, T2>(t2));

        return result;
    }
}

另外,我改变了你的 Choice 类:

// Implement IEnumerable to deal with LINQ
public abstract class Choice<T1, T2> : IEnumerable<Choice<T1, T2>>
{
    IEnumerator<Choice<T1, T2>> IEnumerable<Choice<T1, T2>>.GetEnumerator()
    {
        yield return this;
    }

    public IEnumerator GetEnumerator()
    {
        yield return this;
    }

    // These two methods work with your Devide function
    // I think, it is good to throw an exception here, if c is not a choice of 1
    public static implicit operator T1(Choice<T1, T2> c)
    {
        T1 val;
        c.MatchChoice1Of2(out val);
        return val;
    }

    // And you can add exception here too
    public static implicit operator T2(Choice<T1, T2> c)
    {
        T2 val;
        c.MatchChoice2Of2(out val);
        return val;
    }

    // Your Match method returns void, it is not good in functional programming,
    // because, whole purpose of the method returning void is the change state,
    // and in FP state is immutable
    // That's why I've created PureMatch method for you
    public Choice<T1Out, T2Out> PureMatch<T1Out, T2Out>(Func<T1, T1Out> onChoice1Of2, Func<T2, T2Out> onChoice2Of2)
    {
        Choice<T1Out, T2Out> result = null;

        Match(
            t1 => result = new Choice1Of2<T1Out, T2Out>(onChoice1Of2(t1)),
            t2 => result = new Choice2Of2<T1Out, T2Out>(onChoice2Of2(t2)));

        return result;
    }

    // Continue Choice class
}

您的示例略有不正确,因为当您编写时:

from f1 in ReadDouble().ToChoice("Could not parse input to a double.")
from f2 in ReadDouble().ToChoice("Could not parse input to a double.")
from result in Devide(f1, f2).ToChoice("Cannot devide by zero.")
select result

在最后一行你实际上忽略了 f1 和 f2。所以不可能看到解析错误。写得更好:

        (
            from f1 in ReadDouble().ToChoice("Could not parse input to a double.")
            from f2 in ReadDouble().ToChoice("Could not parse input to a double.")
            from result in
                f1.PureMatch(
                    f1value => f2.PureMatch(
                        f2value => Devide(f1, f2).ToChoice("Cannot devide by zero."),
                        f2err => f2err).Flat(),
                    f1err => f1err
                ).Flat()
            select result
        )
            .Match(
                x => Console.WriteLine("Result = {0}", x),
                x => Console.WriteLine("Error: {0}", x));

你可以创建很好的辅助方法来处理这些复杂的东西,比如 PureMatch 方法,但参数更多

【讨论】:

  • 谢谢,这行得通。但它的行为不像我预期的那样......顺便说一句,我不太喜欢重写隐式运算符,因为这可能会返回 null 或抛出异常......
  • 我认为像这样public static implicit operator Option&lt;T1&gt;(Choice&lt;T1, T2&gt; c) {...} 隐式转换为 Option 会更好
  • “你的 Match 方法返回 void,它在函数式编程中不好” -> 这是一个很好的观点,谢谢!
猜你喜欢
  • 2021-04-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-16
  • 2020-08-05
  • 1970-01-01
  • 2015-06-30
相关资源
最近更新 更多