假设你想在磁盘上加载一个文件并以二进制形式读取文件的数据。从健壮性的角度,需要考虑两种异常情况:加载文件失败,例如给定的文件路径不存在,文件读取文件数据失败,例如磁盘扇区故障,显然,生活总有例外,我们不能乐观地对待它们,要未雨绸缪。只有充分判断这些异常情况,由代码组成的软件系统才能健壮:caseFile.read(path)do{:ok,binary}->case:beam_lib.chunks(binary,:abstract_code)do{:ok,data}->{:ok,wrap(data)}error->errorendererror->errorend代码健壮,但破坏了程序结构的美感。一向贪心的我,自然不会满足于这种扭曲诡异的高质量烂代码。如果代码能够既优雅又健壮,那将是编程世界的乌托邦!这可能不是一个幻想的乌托邦,因为Elixir从1.2版本开始就很贴心地引入了with/1表达式。用它重写以前的代码,整容技术甚至超过了韩国整容,因为整容后的代码不仅漂亮,而且自然,如清水出芙蓉,看来好的代码应该长这样优雅的外观:with{:ok,binary}<-File.read(path),{:ok,data}<-:beam_lib.chunks(binary,:abstract_code),do:{:ok,wrap(data)}有没有穿插嵌套,没有复杂的错误处理语句,就像熟练的雕刻家,几刀,刮掉多余的石头棱角,一张栩栩如生的脸浮现出来,浑然天成。似曾相识?它似乎有一个双胞胎基因与理解。嗯……别被外长给骗了。本质上,for是用来匹配集合中的值(相当于flatMap和filter),而with/1直接匹配值。比如对于定义的两个函数:defok(x),do:{:ok,x}deferror(x),do:{:error,x}对于函数返回值的集合,然后使用模式匹配:ok,可以起到filter的作用:for{:ok,x}<-[ok(1),error(2),ok(3)],do:x#=>[1,3]with会直接作用于函数、正确场景和错误场景根据模式匹配分别处理:with{:ok,x}<-ok(1),{:ok,y}<-ok(2),do:{:ok,x+y}#{:ok,3}with{:ok,x}<-error(1),{:ok,y}<-ok(2),do:{:ok,x+y}#{:error,1}当error(2)无法匹配到{:ok,y}时,with/1的表达式链会及时终止,并返回导致匹配错误的值。这样可以保证不会传递错误的数据,避免不可知的异常。这种做法其实可以解决管道符|>的问题。对于一个执行过程的代码片段,管道符号|>可以让代码充满美感;可惜,动人的作风之下,或许暗藏杀机。在使用管道符时,如果链中的任何一个函数出错,向下传递的数据可能不是下一个函数所期望的,导致整个管道不可控地崩溃。比如我们要写一个发送短信的功能:首先需要获取用户信息,分析要发送的短信内容,然后发送。使用管道字符的代码如下:%{sms:sms,user:nil,response:nil}|>get_user|>get_response|>send_responsedefsend_response(user,response)domessage=user<>response#Assumethatuserandresponse都是字符串send(message)end假设get_response/1有错误,比如返回nil,当代码执行到send_response/2时,可能会抛出ArgumentError。使用with/1可以解决这个问题吗?例如:withuser<-get_user(sms.from),response<-get_response(sms.message),do:send_response(user,response)情况并没有我们想象的那么好,当response为nil时,程序仍然有错误。所以,改成这样:withuser<-get_user(sms.from),response<-get_response(sms.message),sent<-send_response(user,response)dosentelseerror->errorend还是一样!with/1毕竟不是try/catch,它不会捕获执行过程中抛出的错误,然后转向else进行错误处理。只有模式匹配出错才会转向else。这实际上导致了Elixir的一个编程习惯,就是处理异常或者错误的方式。为了优雅地处理错误,将逻辑与优雅的with/1连接起来,需要重构get_user、get_response、send_response等函数。当程序逻辑正确时,返回一个元组对象{:ok,result};如果发生错误,返回{:error,error}。所以代码变成:with{:ok,user}<-get_user(sms.from){:ok,response}<-get_response(sms.message){:ok,sent}<-send_response(user,response)do{:ok,sent}else{:error,:no_response}->send_response(user,"I'mnotsurewhattosay...")error->errorend如果遵循这样的编码标准,则每个函数都不需要检查输入参数是否错误,而是放在with/1的else中处理,可以省去多余的错误处理代码。with/1以一种相对优雅的方式将正常场景与异常场景分开。与使用|>相比,虽然不够直观,但至少保证了代码逻辑结构的清晰,明确体现了Encoding意图,代码还是足够健壮的。你可以两者兼得,with/1几乎达到了这个目的。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文
