【问题标题】:Functional Principles in objectorientated Programs面向对象程序中的功能原则
【发布时间】:2017-01-15 05:41:50
【问题描述】:

今天我看到一段代码,我在想它的意义。所以我做了一个小例子,你可以看到它是如何工作的。 据我所知,它应该是嵌套功能,大量使用 Lambda-Caculus,就像在函数式语言中使用一样。 因为我发现这不太好理解(从文件跳转到文件)我只想知道你们中是否有人对这样的事情有类似的经验。

示例函数一

    public void DoSomethingA(int inputInt, Action<int?, Exception> result)
    {
        try
        {
            int? retVal = inputInt;
            result(retVal, null);
        }
        catch (Exception e)
        {
            result(null, e);
        }
    }

示例函数二

    static void DoSomethingB(int? baseInt, Action<Exception> result)
    {
        try
        {
            result(null);
        }
        catch (Exception e)
        {
            result(e);
        }
    }

两者的调用示例。

    public int? CalculateSomething(int input)
    {
        int? result;

        DoSomethingA(input, (resultInt, aException) =>
        {
            if (aException != null)
            {
                return;
            }

            DoSomethingB(resultInt, bException =>
            {
                if (bException != null)
                {
                    return;
                }

                result = resultInt;
            });
        });
        return null;
    }

也许有趣的是,正常的“主要”函数会有某种承诺。所以它注册了函数并等待结束并得到结果。

【问题讨论】:

  • 显然,您的代码无法编译...在函数 1 中,您调用 result 并使用 null 作为第一个参数,而它需要一个 int。在最后一个代码块中,result 并不总是被初始化。
  • 这里的实际问题是什么?
  • 事实。修复。谢谢!
  • 一个好的函数式实现应该有单独的函数来表示成功和失败。您不必以这种方式将空值传递给成功函数,也不必处理 result 函数中的失败。此外,null 到底是什么意思?什么失败了?文件丢失了吗?是否使用了无效值?
  • 另请注意,Tasks 允许您使用ContinueWith 将结果传递给另一个函数。 async/await 让这个更干净,所以你不需要回调

标签: c# functional-programming design-principles


【解决方案1】:

你所描述的是一个 Monad。特别是Try monad。我将在下面展示如何实现 Try monad,您的 CalculateSomething 代码将如下所示:

public Try<int> CalculateSomething(int input) =>
    from x in DoSomethingA(input)
    from y in DoSomethingB(x)
    select y;

无论如何,这很容易理解。

首先声明一个代表 Try 操作的委托:

public delegate TryResult<T> Try<T>();

接下来定义TryResult&lt;T&gt;。成功时会捕获返回值,失败时会捕获失败异常:

public struct TryResult<T>
{
    internal readonly T Value;
    internal readonly Exception Exception;

    public TryResult(T value)
    {
        Value = value;
        Exception = null;
    }

    public TryResult(Exception e)
    {
        Exception = e;
        Value = default(T);
    }

    public static implicit operator TryResult<T>(T value) =>
        new TryResult<T>(value);

    internal bool IsFaulted => Exception != null;

    public override string ToString() =>
        IsFaulted
            ? Exception.ToString()
            : Value.ToString();

    public static readonly TryResult<T> Bottom = new InvalidOperationException();
}

接下来为Try委托定义一些扩展方法(这是C#中一个鲜为人知的特性,委托支持扩展方法):

public static class TryExtensions
{
    public static TryResult<T> Try<T>(this Try<T> self)
    {
        try
        {
            if (self == null) return TryResult<T>.Bottom;
            return self();
        }
        catch (Exception e)
        {
            return new TryResult<T>(e);
        }
    }

    public static R Match<T, R>(this Try<T> self, Func<T, R> Succ, Func<Exception, R> Fail)
    {
        var res = self.Try();
        return res.IsFaulted
            ? Fail(res.Exception)
            : Succ(res.Value);
    }
}

它们都允许以安全的方式调用Try 委托。关键是Match 扩展方法,它在结果上“模式匹配”:

int res = CalculateSomething(1).Match(
              Succ: value => value,
              Fail: excep => 0 
              );

因此,您不得不承认该函数可能会抛出异常以获取该值。

这里缺少的一件事是它如何与 LINQ 一起工作:

public static class TryExtensions
{
    public static Try<U> Select<T, U>(this Try<T> self, Func<T, U> select) =>
        new Try<U>(() =>
        {
            var resT = self.Try();
            if (resT.IsFaulted) return new TryResult<U>(resT.Exception);
            return select(resT.Value);
        });

    public static Try<V> SelectMany<T, U, V>(
        this Try<T> self,
        Func<T, Try<U>> bind,
        Func<T, U, V> project ) =>
            new Try<V>(() =>
            {
                var resT = self.Try();
                if (resT.IsFaulted) return new TryResult<V>(resT.Exception);

                var resU = bind(resT.Value).Try();
                if (resU.IsFaulted) return new TryResult<V>(resT.Exception);

                return new TryResult<V>(project(resT.Value, resU.Value));
            });
}

Select 允许这个工作:

var res = from x in y
          select x;

SelectMany 允许这个工作:

var res = from x in y
          from z in x
          select z;

也就是说,它允许多个 from 语句按顺序运行。这被称为 monadic 绑定(但你不需要知道它才能工作 - 我宁愿不在这里写一个 monad 教程)。从本质上讲,它是在您的 CalculateSomething 示例中捕获嵌套模式,因此您无需再次手动编写它。

就是这样。上面的代码,你只写一次。现在让我们实现你的DoSomething 函数:

public Try<int> DoSomethingA(int inputInt) => () =>
   inputInt;

public Try<int> DoSomethingB(int inputInt) => () =>
{
    throw new Exception();
};

注意它们是如何定义为 lambdas 的。如果您使用的是 C# 5 或更低版本,它们将如下所示:

public Try<int> DoSomethingA(int inputInt)
{
    return () => inputInt;
}

如果您想查看更完整的实现,请查看我的language-ext functional library for C#,那里有Try implementation。它有许多更有用的扩展方法,允许您编写函数式代码而无需 try/catch 的样板。

【讨论】:

  • 很棒的答案!同时复杂和简单......将使用它来实现我认为的当前结构的新实现。
【解决方案2】:

好吧,有时遵循良好设计的代码可能比单一功能更难理解,但重用代码的某些部分并避免代码重复要容易得多......

显然,通过正确命名对象和函数,代码应该更容易理解。

在上面的代码中,最好忽略函数 1 和 2 中的异常并在调用者处处理它们。这样,动作就被简化了。例如,以下代码更简单但等效:

public int? CalculateSomething(int input)
{
    try
    {
        var result1 = SomeMath(input);
        var result2 = SomeOtherMath(result1);
        return result2;
    }
    catch
    {
        return null;
    }
}

并且该代码可以更容易地泛化以处理操作列表,其中一个函数的结果是下一个函数的输入。

【讨论】:

    猜你喜欢
    • 2012-07-28
    • 1970-01-01
    • 2019-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-26
    • 2023-03-23
    • 2017-03-24
    相关资源
    最近更新 更多