没专门语法支持的语言里用monad好难看

上周在JavaEye问答看到[url=http://www.iteye.com/problems/12421]求一个逻辑运算结果[/url],其中De Morgan定律的应用如night_stalker老兄所说,并不困难。不过就这么应用定律来推算或许不直观,所以当时我就想换个角度,写段C#代码来穷举验证问题中的表达式在应用De Morgan定律前后的真假值是否总是相同。

通过LINQ,代码很简洁:
using System;
using System.Linq;

static class Program {
static void Main(string[] args) {
var booleanValues = new [] { true, false };
var equal = !(from a1 in booleanValues
from a2 in booleanValues
from a3 in booleanValues
from b1 in booleanValues
from b2 in booleanValues
from b3 in booleanValues
select (!((a1 && b1) || (a2 && b2) || (a3 && b3))) ==
(!(a1 && b1) && !(a2 && b2) && !(a3 && b3)))
.Contains(false);
Console.WriteLine(equal); // true
}
}

这段代码看起来应该算是比较直观的。不过……似乎又不太直观?

对许多人来说,或许显式用循环会直观得多:
using System;

static class Program {
static void Main(string[] args) {
var booleanValues = new [] { true, false };
var equal = true;
foreach (var a1 in booleanValues) {
foreach (var a2 in booleanValues) {
foreach (var a3 in booleanValues) {
foreach (var b1 in booleanValues) {
foreach (var b2 in booleanValues) {
foreach (var b3 in booleanValues) {
if ((!((a1 && b1) || (a2 && b2) || (a3 && b3))) !=
(!(a1 && b1) && !(a2 && b2) && !(a3 && b3))) {
equal = false;
break;
}
}
if (!equal) break;
}
if (!equal) break;
}
if (!equal) break;
}
if (!equal) break;
}
if (!equal) break;
}
Console.WriteLine(equal); // true
}
}

但这代码好丑啊 =v=
好吧我作 弊了。就以上两个版本的代码来比较,它们实际的执行过程是不同的;LINQ的版本是会把所有可能性都算出来之后再看是否Contains(),因为Contains是一个eager operator,不像Select、Where那些是lazy的;循环版是遇到不相等的状况就直接退出循环了。不过反正两个版本算出来的结果都一样,用来对比也不算过分。

其实C#的foreach循环已经掩盖了许多细节,不需要显式用下标或者IEnumerator来遍历容器。不过我还是不喜欢在这种场景用显式的循环,主要是因为循环嵌套起来非常难看,搞不好就要超过屏幕的右边了。

之前说LINQ的版本看起来简洁,但未必很直观,是因为:多个from子句连在一起对应的函数是SelectMany,对IEnumerable<T>/IQueryable<T>而言这个LINQ运算符背后的概念则是list monad中的bind函数;其中的机理还是需要读些资料才能理解的。一看到monad这词很多人就要开始晕了吧? =w=

本来是想list monad在C#通过LINQ来使用可以这么简洁,那要是能在Ruby里也这样用就好了。但如标题所说,语言没有提供monad语法的时候,这玩儿写起来也不优雅。其实更关键的问题或许是没有typeclass的条件下monad无法提供统一的接口来提高抽象层次?不过我只想关心语法,也就是“看起来如何”的问题。

例如说,用Ruby来实现一个maybe monad:
class Maybe
def initialize(val, has_val = true)
@value, @has_value = val, has_val
end

def value
raise 'Maybe::None has no value' unless @has_value
@value
end

class << self
def unit(val)
new(val).freeze
end
end

def bind
@has_value ? (yield @value) : Maybe::None
end

def none?
self == Maybe::None
end

def clone
raise TypeError, "can't clone instance of Maybe"
end

def dup
raise TypeError, "can't dup instance of Maybe"
end

None = Maybe.new(nil, false).freeze

private_class_method :new, :allocate
end

定义了Maybe类之后,可以这样用:
res1 = Maybe.unit(1).
bind { |x| Maybe.unit(x + x) }.
bind { |x| Maybe.unit(x * x) }
#=> #<Maybe:0x34bc3e8 @has_value=true, @value=4>

看起来还不错,方法调用没有嵌套,都是串在一起的。
但这只不过是因为这一连串计算是从单一的来源得到数据的:那个“1”。假如要把一个“1”和一个“2”都用Maybe包装起来,然后要得到它们的和,那就变成:
one, two = [1, 2].map { |i| Maybe.unit i }
res2 = one.bind { |x|
two.bind { |y|
Maybe.unit(x + y)
}
}
#=> #<Maybe:0x35ece5c @has_value=true, @value=3>

于是bind的调用就嵌套起来了,郁闷 T T

我一时觉得纳闷,明明记得在Haskell里写的时候即使不用do也没这么麻烦的啊,像这样:
Just 1 >>= \x -> Just 2 >>= \y -> return (x + y)

然后才想起来其实这个表达式就是嵌套的 OTL
(Just 1) >>= (\x -> ((Just 2) >>= (\y -> (return (x + y)))))

还是没有括号看起来顺眼些……但是由于优先级不同,Haskell里能省略的这些括号在Ruby里就省略不了。只好乖乖的写吧。

嘛,无论如何,有了Maybe类的定义后,我们就得到了传播Maybe::None的能力。像是把前面的1+2换成1+None,得到的就会是None:
res3 = one.bind { |x|
Maybe::None.bind { |y|
Maybe.unit(x + y)
}
}
#=> #<Maybe:0x316384 @has_value=false, @value=nil>
res3.none?
#=> true

或者写在一行上?
res3 = one.bind { |x| Maybe::None.bind { |y| Maybe.unit(x + y) } }

用Haskell写的话,不用do记法是
Just 1 >>= \x -> Nothing >>= \y -> return (x + y)

用do记法的话
do x <- Just 1
y <- Nothing
return (x + y)


嗯,看过maybe monad,那么list monad呢?
Ruby里的Enumerable模块其实就可以看成一个抽象的表,里面的select、inject、map/collect等方法都有函数式编程的对应物:filter、fold_left、map等。那干脆就把list monad的bind函数写在Enumerable里好了,像这样:
module Enumerable
def bind
self.inject([]) { |acc, e| acc + (yield e) }
end
end

然后,例如说要求[1, 2]的每个元素分别与[3, 4, 5]的每个元素的乘积的列表:
[1, 2].bind { |x|
[3, 4, 5].bind { |y|
[x * y]
}
}
#=> [3, 4, 5, 6, 8, 10]

呜,没想像中的好看 T T
即使不用list monad,直接用each来写看起来也差不多:
res = []
[1, 2].each { |x|
[3, 4, 5].each { |y|
res << (x * y)
}
}
#=> res == [3, 4, 5, 6, 8, 10]


而在有专门的语法支持的语言里,这逻辑写起来就很优雅:
do x <- [1, 2]
y <- [3, 4, 5]
return (x * y)
-- [3,4,5,6,8,10]

from x in new [] { 1, 2 }
from y in new [] { 3, 4, 5 }
select x * y
//=> { 3, 4, 5, 6, 8, 10 }

在Haskell里即使不用do记法也不难看:
[1, 2] >>= \x -> [3, 4, 5] >>= \y -> return (x * y)


回到开头C#的那个例子,用上面Ruby里的list monad来写的话,
boolVals.bind { |a1|
boolVals.bind { |a2|
boolVals.bind { |a3|
boolVals.bind { |b1|
boolVals.bind { |b2|
boolVals.bind { |b3|
[(!((a1 && b1) || (a2 && b2) || (a3 && b3))) ==
(!(a1 && b1) && !(a2 && b2) && !(a3 && b3))]
}
}
}
}
}
}.all? { |b| b }
#=> true

感觉微妙……

有没有什么办法能让这monad在Ruby里更好看些?还是说我要求的过多了?
即使像LINQ的SelectMany那样把Bind写成多接受一个参数的版本,调用也还是会嵌套啊(虽然lambda表达式不必嵌套了)。我就是想少写点括号少写些缩进而已……
周末再读读《The Ruby Programming Language》来找找灵感好了。

对了,记个链接:[url=http://moonbase.rydia.net/mental/writings/programming/monads-in-ruby/00introduction.html]MenTaLguY: Monads in Ruby[/url]
还没来得及读……
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 不是的,面向对象和命令式语言支持并不是决定因素。Monad是一种编程范式,它主要用于处理程序中的复杂逻辑。而不管是面向对象语言还是命令式语言,都可以实现Monad的概念。 在Python中,Monad是可选的,你可以使用或不使用它。有些情况下使用Monad可以让代码更简洁易懂,有些情况下则不适用。因此,决定使用Monad还是不使用,取决于实际的需求和开发人员的偏好。 ### 回答2: 不完全是。Python是一种多范式的编程语言,同时支持面向对象和命令式编程。虽然Python支持面向对象,但它在本质上仍然是一种命令式语言,因为它按照指令的顺序执行代码。 Monad是一种设计模式,主要用于处理副作用和状态,以及创建可组合的函数。虽然Python不需要monad支持面向对象编程,但在处理副作用和状态时,monad可以提供一种优雅的解决方案。例如,在函数式编程中,monad可以用于处理异常、处理IO操作等,通过将这些操作包装在monad中,可以更好地管理副作用。 尽管Python本身已经提供了一些用于处理副作用和状态的机制,比如`try-except`来处理异常,`with`语句来处理资源管理,但这些机制在复杂场景下可能会变得笨重。使用monad可以更好地组织和管理这些副作用,提供更加清晰和模块化的代码结构。 因此,尽管Python不需要monad来实现面向对象编程,但在某些场景下,使用monad可以提供更好的代码组织和管理副作用的方式。使用monad可以使代码更易于理解和维护,特别是在处理复杂的状态和副作用时,它可以提供一种更高层次的抽象和解决方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值