我们已经学习了怎样创建一个简单的Monad, MaybeMonad, 并且知道了它如何通过在 Bind函数里封装处理空值的逻辑来移除样板式代码. 正如之前所说的,我们可以在Bind函数中封装更复杂的逻辑. 下面给出一个更复杂更典型的Monad例子,一个解析器Monad. 在本篇将要介绍一个解析器,在之后的篇幅里将会把解析器转换成一个 Monad.
首先我们思考解析器要完成什么功能,它接受一个输入,通常是一些文本,然后输出期望的结果. 因此一个CSV解析器将会接受一个文本文件,输出行和列的数据,并带有数据类型. 我们可以把parser抽象成一个函数 ,它接受一个string,返回某种类型 T:
Func<string,T>
将一个大的任务分解成小的任务实现通常会更简单,所以如果我们能同坐组合很多小的解析器来构建我们的解析器会更好. 每个小的解析器可能会消耗部分字符串,所以我们可以定义函数接受一个 string, 返回 T 和匹配后剩余的字符串
Func<string, Tuple<T,string>>
Tuple是在.Net4里引入的新类型, 你可以用自定义类型替代。
我们的解析器可能并不能正确解析输入的字符串,因此我们还需要能处理解析失败的情况, 这里可以使用我们已定义的 Maybe 类型
Func<string, Mayb<Tuble<T,string>>>
现在来创建一个比较简单的解析器, 它匹配字符串"Hello":
public static Maybe<Tuple<T,string>>FindHello(string input) { return input.StartWith("hello") ?new Just<Tuple<string,string>>(Tuple.Create("Hello",input.Skip("Hello".Length).AsString())) :(Maybe<Tuple<string,string>>)new Nothing<Tuple<string,string>>(); }
如果输入的字符串中包含"Hello",将会返回"Hello"和剩余的字符串
var result=Parsers.FindHello("Hello world"); var justResult= result as Just<Tuple<string,string>>; Console.WriteLine("justResult.Value.Item1={0}",justResult.Value.Item1); //justResult.Value.Item1=Hello Console.WriteLine("justResult.Value.Item2={2}",justResult.Value.Item2); //justResult.Value.Item2=World
如果我们输入"GoodBye" ,它将会返回Nothing:
var result2=Parsers.FindHello("Goodbye world"); Console.WriteLine("resulte2={0}",result2); //result2=Nothing
通过创建一个可以解析任何字符串的解析器工厂,我们可以使我们的解析器更优美, 首先定义一个delegate:
public delegate Maybe<Tuple<T,string>>Parser<T>(string input)
现在定义Find,写在扩展方法里:
public static Parser<string>Find(this string stringToFind) { return input=> input.StartsWith(stirngToFind) ?new Just<Tuple<string,string>>(Tuple.Create(stringToFind,input.Skip(stringToFind.Length).AsString())) :(Maybe<Tuple<string,string>>)new Nothing<Tuple<string,string>>(); }
这是一个高阶函数,它返回一个函数,就是我们的解析器, 注意我们的解析器是一个delegate。
现在我们可以用它创建一些解析器,比如一个"Hello" 解析器和一个"World"解析器
var helloParser= "Hello".Find(); var worldParser="World".Find();
我们加一个扩展方法方便把解析结果转换成string:
public static string AsString<T>(this Maybe<Tuple<T,string>>parseResult, Func<T,string>unwrap) { var justParseResult= parseResult as Just<Tuple<T,string>>; return (justParseResult != null ?unWrap(justParseResult.Value.Item1)) :"Nothing"; }
现在我们用我们的helloParser 和 worldParser来解析字符串 :
var result =helloParser("Hello World").AsString(s=>s); Console.WriteLine("result = {0}", result); //result = Hello var result2= worldParser("World Hello").AsString(s=>s); Console.WriteLine("result2={0}",result2); //result=World
我们怎么能把这两个parser结合起来创建一个"HelloWorld"的parser呢,这里有一个生硬的实现:
Parser<Tuple<string,string>>helloWorldParser=input=> { var helloResult= helloParser(input) as Just<Tuple<string,string>>; if(helloResult == null) return new Noting<Tuple<Tuple<string,string>,string>>(); var worldResult= worldParser(helloResult.Value.Item2) as Just<Tuple<strin,string>>; if(worldResult == null) return new Noting<Tuple<Tuple<string,string>,string>>(); return new Just<Tuple<Tuple<string,string>,string>>(Tuple.Create( Tuple.Create(helloResult.Value.Item1,worldResult.Value.Item1),worldResult.Value.Item2)); }; var result3=helloWorldParser("HelloWorld").AsString(s=>s.Item1 + " " + s.Item2); Console.WriteLine("result3 = {0}",result3); //result=Hello World
这样写非常繁琐,假如我们要组合更复杂的解析器,比如CSV解析器,将会非常令人头疼.但是不要担心,在下一篇我们将会把我们的解析器转换成Monad, 并以非常简单的方式组合他们