在上一讲中,我们学习了在函数式编程中一个,非常重要的概念。藉由这个武器,我们将来挑战我们最开始做讲述的概念,代码的副作用。当然,如果你还记得的话,程序的异常也是副作用的其中一种。
复习时间:会影响外界状态(状态突变)、使用到了IO(输入输出)的功能、改变输入参数,或是抛出异常叫做副作用
如果你觉得有部分知识点已经忘了,可以回顾一下之前的内容
在本期开始之前需要安装LanguageExt库(nuget)
并引入以下两行
using LanguageExt;
using static LanguageExt.Prelude;
单子在FP之中有一个重要的任务,那就是管控副作用的范围。这也是我们一直想要强调的,副作用在程序中不可避免。但我们不应该让副作用在程序中到处都是,而是要牢牢地把控住其能影响的范围。副作用不可或缺,但其存在具有两面性。而单子的存在,能最大限度的减少其不好的一面。
之前我们已经了解到了Option这个单子,今天我们还将遇见一个新的单子
我们先来想象一下option是否真的够用
Either<L,R>
这是我们新的单子!也有称之为Result(Result<Ok, Error>(F#就是如此)),不过Result在LanguageExt中有另外一个意义,这里我们还是用Either作为介绍。
Either内部有两类值,一般我们称之为Left和Right。
当然按照字面意义,Left会写在左面,而Right在英文中也有正确的意思,所以Either代表的是一个成功的值Right或者一个失败的信息Left。
相比于Option,Either能够传达更多更详细的信息,所以在实际使用时可能会见到更多的使用Either在代码中进行流转。(因为option只能表达数据的缺失这一种状态)
事不宜迟,我们先来写一下Either的Bind和Map的函数签名...

欸等等? Either有两个有效数据我们应该怎么写?
确实是这样,不过通常情况下我们只需要关心Right的数据即可
bind: (Either<L, R>, (R -> Either<L, R1>)) -> Either<L, R1>
map: (Either<L, R>, (R -> R1)) -> Either<L, R1>
以下是Map与Bind的实际实现(当然 安装了库的我们不需要手动自己实现)
public static Either<L, R1> Map<L, R, R1>(this Either<L, R> either,
Func<R, R1> f) =>
either.Match<Either<L, R1>>(
Right: r => Right(f(r)),
l => Left(l)
);
public static Either<L, R1> Bind<L,R,R1>(this Either<L, R> either,
Func<R, Either<L, R1>> f) =>
either.Match<Either<L, R1>>(
Right: r => f(r),
l => Left(l)
);
同样的非常简单,当Either中值为Left的时候,bind,map都不会再影响到Either的值,当然如果你想对其中的Left值进行操作,试着实现一下MapLeft与BindLeft!
现在我们得到了一种保存错误情况下信息的方式,我们还有必要在代码中抛出错误吗?throw会肆意的中断我们的程序,这其实在大部分时候往往不是我们所想要的。试着思考一下,当我们去问一个问题的时候,我们希望得到什么样的回复。
如果能得到正确的回复当然最好不过,如果无法解决的时候,你希望获得的是
1. 一个什么都没有的回复(Option.None)
2. 一个告诉你为什么问题不能回答的原因(Either.Left(reason))
3. 不回复,然后直接打了一拳,但你可以在医院里知道打你造成的结果(throw)
你会选择什么呢?相信答案显而易见,当然可能对throw的描述略显夸张,但抛出错误的语义可能会跨越非常大的代码范围,如果没有处理,也可能直接使程序崩溃。这不是一件好事,我们需要控制他的范围。
实战
现在来看一种情况,你是否会遇到过这种类似这种每一步都需要尝试校验,最终才能得到结果的代码
bool DoSth1(int id)
{
var equip = Equipment.Get(id);
if (equip == null)
{
throw new Exception("No equipment found");
}
var open = equip.Open();
if (!open)
{
throw new Exception("open failed");
return false;
}
var dosth = equip.DoSth();
if (!dosth)
{
throw new Exception("dosth failed");
return false;
}
var close = equip.Close();
if (!close)
{
throw new Exception("close failed");
return false;
}
return true;
}
难以想象的复杂,开发者最后有可能最后为了缩减代码,将计就计,让错误直接在方法内部调用的时候就直接抛出。这是一种方法,但是毫无疑问,这会造成全局乱成一锅粥,随便调用一个接口都要承担程序直接崩溃的风险!
而如果你使用了Either, 并聪明的使用了Bind去连接函数操作,你会发现你得到了相当简洁的一个代码,并且不会有任何崩溃的风险
Either<string, EquipmentF> DoSth(int id) =>
id.Get()
.Bind(EquipmentExtensions.Open)
.Bind(EquipmentExtensions.Pre)
.Bind(EquipmentExtensions.DoSth)
.Bind(EquipmentExtensions.Close);
当我们拿到结果后,我们会得到一个可能的结果,这个结果可能是Right, 那么我们将拿到正确的值。也可能是Left,我们会拿到最早导致错误的信息。当然既然错误了,错误之后的步骤自然没有执行。
以下便是FP中代码运行的流程

(也有将形如这种的编程模式称之为面向铁路编程)
可以看到,新的方法让我们回避了多次无聊的判断,并且让程序的出口也只变成了唯一一个,同时如果我们希望的话,也可以直接使用=>来返回最终的结果。我们成功利用了单子,将错误牢牢地管控在了一个很小的范围中,不必担心突如其来的程序无法处理的错误。
try..catch
即使圣人不偏不倚的走在了正确的道路上,也依然难以避免这样或者那样的错误。
没错事实上很多异常我们没有办法避免,我们还是需要Try来帮助我们捕获一些异常。
不过在C#之中,try是一个语句。这使得try的语句连贯性不高,我想编写过类似代码的同学一定有类似的体验
var t = SomeMethod(); // 这步可能出错
DoSth(t);
如果SomeMethod是一个可能出错的代码,并且我们也没有办法对SomeMethod本身进行重构。我们可能需要try来帮我们捕获代码的错误。我们可能不得不这么修改
SType t = null; // 这步可能出错
try
{
t = SomeMethod();
}
catch (Exception e)
{
throw;
}
DoSth(t);
太难看了!
我们不仅要捏着鼻子将一个null赋值,最终有可能还是会抛出错误。代码整体也变得特别割裂,我相信大家都不想经常写这么难看的代码,这也是我们排斥语句的原因之一。
那么根据我们学到的内容来说,我们更期待一种怎么样的写法呢?
我们想到了刚刚所说的Either,在这种情况下,try中有可能有一个有效的值,或是一个错误。根据定义我们可以得出一个类型Either<Exception, R>, 他的左值总是一个错误类型,这可以是一个Either的特化类型,Exceptional<T>, (在LanguageExt中,它叫做Result<T>),现在有了我们想要的结果的类型,接下来要做的,则是规避掉这个疯狂的语法,转换为语句
我们当然想要这么写
var t =
try
{
return SomeMethod();
}
catch (Exception e)
{
return e;
}
DoSth(t);
这种写法,清晰,明确,且没有复杂的赋值顺序,这在F#有类似的实现,但在C#之中,我们没有类似的语法帮我们实现这一点。所以我们需要自己创建一个函数帮我们解决这一切。
其实我们需要的只是一段代码的封装, 接下来写出我们所需要的函数签名
Run<T>: (()->T) -> Exceptional<T>
我们将这个()->T声明为Try<T>, 并编写出Run/Invoke函数
public delegate Result<T> Try<T>();
public static Result<T> Run<T>(this Try<T> f)
{
try
{
return f();
}
catch (Exception e)
{
return new(e);
}
}
我们简单的Try就完成了,在LanguageExt中,Try也提供Bind和Map函数,所以同样也是一个单子,有兴趣的同学,也可以尝试实现这一个工作
在F#中,我们直接支持try作为表达式使用,非常方便。
验证器
如果仔细观察Exception, 可以注意到,Exception都是一些意料之外的错误,其实在代码中我们还可能遇到一些错误,这些错误不是致命性的,但是可能有悖业务逻辑,例如我们需要判定某人年龄是否大于18岁,或是是否是教师。这些意料之内的不合理,我们更常使用验证器来对齐收集,这是另一个版本的Etiher,我们通常用它来收集数据/逻辑不合理的等情况,
这时候我们可能会使用另一个Either变体 Validation,
Either<IEnumerable<Error>,T>
篇幅问题在本片就不继续衍生了,在之后的实战演练中,我们会更深刻的理解这部分内容。
Bilibili:@无聊的年
微信公众号:@scixing的炼丹炉

873

被折叠的 条评论
为什么被折叠?



