循序渐进学Boo - 知识篇
Boo官方网站上提供了详细的介绍与实例(尽管有小部分内容现已过时,但大家还是可以学习到Boo的语法特性):
1.Boo Primer
2.Language Guide
如果大家希望能深入了解到Boo的语法特性,强烈建议大家阅读Steve Donovan写的The Book of Boo - A TiddlyWiki about Boo。
在大家阅读完上述链接的内容后,本文着重结合"从一个小实例开始 - Boo开篇"中的实例向大家介绍Boo相对重要的特性,为后续文章的阅读奠定基础。
首先要强调的是,编写Boo代码需要遵循以下的代码结构顺序:
module docstring
namespace declaration
import statements
module members: class/enum/def declarations
main code executed when script is run
assembly attributes
下面是个例子:
""" module docstring """ namespace My.NameSpace # optional namespace declaration import System # import statements # followed by the Members of this module (classes, methods, etc.) class Heater: [Getter(Manufacturer)] _manufacturer = "Siemens" [Property(Temperature, Temperature > 0)] _temperature as int def Boil(): pass # 如果不想有具体实现,可以使用关键字pass # start main section that is executed when script is run heater = Heater() heater.Temperature = 96 # optional assembly attribute declarations used when compiling [assembly: AssemblyTitle('Title')] [assembly: AssemblyDescription('Description')]
一、Classes
正如上面例子看到在Boo中很容易进行类的定义与实现。在Boo的类定义中,可以很容易使用Property、Getter、Setter这三个attributes进行字段的属性设置。
其中Property还提供了第二个参数,可以用它指定Pre-condition,只有符合这个条件时才可以进行setter的操作:
[Property(Temperature, value > 0)]
_temperature as int
此外之外,我们也可以使用自定义的attribute来进行扩展(通过AbstractAstAttribute)。
类的默认修饰符是public,类中所有的字段默认都是 protected,所有的方法、属性和事件默认都是 public。
Boo 提供了一个非常有用的特性,它让你可以在实例化对象时直接指定属性的值。
heater = Heater(Temperature: 95, Manufacturer: 'Philips')
在Boo中继承同样很简单:
abstract class Device: abstract def Do(): pass class Alarm(Device): override def Do(): print "Alarm!"
二、AbstractAstAttribute
其命名的惯例是:"属性名称"+Attribute。
下面是一个简单的自定义属性(AbstractAstAttribute)例子,它继承自Boo的PropertyAttribute(继承于AbstractAstAttribute):
import Boo.Lang.Compiler import Boo.Lang.Compiler.Ast class NonEmptyStringAttribute (PropertyAttribute): static preCondition = [| value is not null and len(value) > 0 |] def constructor(propName as ReferenceExpression): super(propName,preCondition.CloneNode()) class Person: [NonEmptyString(Name)] # line 4 _name as string [property(Age)] _age as int override def ToString(): return _name p = Person(Name:"john",Age:36) p.Name = '' # line 14 print p
相比复杂的函数调用,AST literals则提供了一个更简洁、更友好的方式用以构建Boo Ast nodes。
// using constructor calls. mie = MethodInvocationExpression(ReferenceExpression('print'), StringLiteralExpression('Hello,world')) // ast literals allow you to say: mie = [| print "Hello,world" |]
也就是说,你可以直接从Boo代码中构建Ast对象。
【注】:这里[| print "Hello,world" |]被称为Quasi-Quotation。
下面是详细的英文解释:A quasi-quotation evaluates its body as a code tree expression.
This is a special type of operator that tells Boo that whatever is inside of the block is to be parsed and returned as an AST(abstract syntax tree) node.
$ is generally called the "splice" operator and it means "evalute me at compilation time".
The splice application $(condition.ToCodeString()) automatically lifts the string value returned by ToCodeString to a proper StringLiteralExpression.
下面是前一篇文章中使用AbstractAstAttribute的例子:
import System import Boo.Lang.Compiler import Boo.Lang.Compiler.Ast # AST Attributes inherit from AbstractAstAttribute class EnsureAttribute(AbstractAstAttribute): # The expression that we were supplied # note that this happens during compilation expr as Expression # Store the expression in a field def constructor(expr as Expression): self.expr = expr # Now we get the chance to make the changes that we want def Apply(target as Node): # we cast the target to a ClassDefinition and # start iterating over all its members type as ClassDefinition = target for member in type.Members: method = member as Method continue if method is null # if the member is a method, we modify # it to include a try/ensure block, in which # we assert that the expression that we got must # be true. Then we override the method body with # this new implementation. block = method.Body method.Body = [| block: try: $block ensure: assert $expr |].Body # 使用方法如下: [Ensure(name is not null)] class Customer: name as string def constructor(name as string): self.name = name def SetName(newName as string): name = newName
【注】:
Boo AstAttributes explained有详细的描述,对大家的理解会有很大帮助。
三、AbstractAstMacro
Boo中的Macros是在编译过程阶段被编译器扩展,并根据相应的参数和代码转换为AST以实现Boo编译器扩展的能力,因此通常用Macros来创建你自己的关键字。
在 Boo 里的某些述句,像 print 和 using,其实都是通过Macro来实现的。Macro 可以接受参数Arguments,也可以拥有代码块Block。其语法如下:
SomeMacro arg1, arg2, ...:
block
值得注意的是,收到的参数并不能直接Evaluate使用,它们会被编译为 AST (Abstract Syntax Tree)表达式以进行操作,也就是说,参数并没有真正被赋值(evaluate)。
Macro 通常用来产生代码,它们会在编译时期以真正的代码取代。
如同自定义属性(AbstractAstAttribute)一样,Macro的名称必须以 'Macro' 结尾。
要使用自定义Macro,你遇到最大的问题在于必须对 AST有相当程度的了解,要建立运算式和述句,你需要了解编译器如何表现它们。
由于Macro正是Boo的优势所在,随后我会再写一篇文章希望可以详细介绍它。
在上篇文章最后部分我给出了大师的代码,其中unroll和unroll2就是分别用不同的方法实现Macro(其实本质是一样的):
// Method 1 import System import Boo.Lang import Boo.Lang.Compiler import Boo.Lang.Compiler.Ast # We create a class for the macro, the class name is # meaningful, [macro name]Macro allows us to later refer # to the macro using just [macro name]. # Note that we inherit from AbstractAstMacro class UnrollMacro(AbstractAstMacro): # Here we perform the actual compiler manipulation # the compiler hands us a macro statement, and we have # to return a statement back, which will replace it. def Expand(macro as MacroStatement) as Statement: # define a block of code block = Block() # extract the second parameter value end = cast(IntegerLiteralExpression, macro.Arguments[1]).Value for i in range(end): # create assignment statement, using the block: trick and add it to # the output statements = [| block: $(macro.Arguments[0]) = $i |].Body block.Add(statements) # add the original contents of the macro # to the output block.Add(macro.Body) return block // Method 2 import System import Boo.Lang import Boo.Lang.Compiler import Boo.Lang.Compiler.Ast # Using the MacroMacro, we don't need a class, # just to define what we want the macro to do macro Unroll2: # define a block of code block = Block() # extract the second parameter value end = cast(IntegerLiteralExpression, Unroll2.Arguments[1]).Value for i in range(end): # create assignment statement, using the block: trick and add it to # the output statements = [| block: $(Unroll2.Arguments[0]) = $i |].Body block.Add(statements) # add the original contents of the macro # to the output block.Add(Unroll2.Body) return block # 使用方法如下: unroll i, 5: print i unroll2 i, 5: print i
最后大家看看下面这个简单的Macro,看看它是做什么用的:
import System import Boo.Lang.Compiler import Boo.Lang.Compiler.Ast class AssignFieldsMacro(AbstractAstMacro): override def Expand(macro as MacroStatement): ctor = macro.GetAncestor(NodeType.Constructor) as Constructor b = Block() for param in ctor.Parameters: assign = BinaryExpression(BinaryOperatorType.Assign, ReferenceExpression("_" + param.Name), ReferenceExpression(param.Name)) b.Add(ExpressionStatement(assign)) return b
【注】:
Boo AstMacros explained里有详细描述。
四、Meta Method
如果你不需要操作AST nodes,那么可以使用meta method代替Macros。
import System import Boo.Lang.Compiler.Ast class MetaMethods: [meta] static def verify(expr as Expression) as UnlessStatement: return [| unless $expr: raise $(expr.ToCodeString()) |]
当编译这段Boo代码发现了verify关键字时,这个方法会通知编译器用下面
[|
unless $expr:
raise $(expr.ToCodeString())
|]
生成的实际代码来替换。
五、Extension Method
可以用来往已有的类型中添加新的方法,类似于C#中的Extension Method。请看前面的例子:
class MyExtensions: # Creating an extension methods so 200.ms will be valid [Extension] static ms[i as int]: get: return TimeSpan.FromMilliseconds(i) # 使用方法: limitedTo 200.ms: ......
六、IQuackFu
有时候如果能让类自己负责方法、属性的调用,将会非常有用。任何实现了 IQuackFu 接口的类都必须做到:
1.如何调用方法、2.如何设定属性、3.如何读取属性。
在Steve Donovan写的例子中,通过IQuackFu接口实现了类似JavaScript中对象的存取方式:
class Map(IQuackFu): _vars = {} def QuackSet(name as string, parameters as (object), value as object) as object: _vars[name] = value def QuackGet(name as string, parameters as (object)) as object: return _vars[name] def QuackInvoke(name as string, args as (object)) as object: pass e = Map() e.Alice = 32 e.Fred = 22 e.John = 42 print e.Alice,e.Fred,e.John
还有前文中的XmlObject,它是实现了IQuackFu接口的类,因此可以根据节点名称找到相应的值:
class XmlObject(IQuackFu): # Implementing IQuakcFu interface _element as XmlElement # The element field # Get the xml element in the constructor and store it a field def constructor(element as XmlElement): _element = element # We do not support invoking methods, so we just ignore this # method. We could also raise NotSupportedException here. def QuackInvoke(name as string, args as (object)) as object: pass # ignored # If we wanted two way communication, we could have built # it into this method, but we aren't, so we are ignoring this # method as well def QuackSet(name as string, parameters as (object), value) as object: pass # ignored # Here we intercept any property call made to the object. # This allows us to translate a property call to navigating # the XML tree. def QuackGet(name as string, parameters as (object)) as object: # Get the node(s) by its name elements = _element.SelectNodes(name) if elements is not null: # Check that the node exists # Here we are being crafy, if there is only one node # selected, we will wrap it in a new XmlObject and # return it, this allows us easy access throughout # the DOM. return XmlObject(elements[0]) if elements.Count == 1 # If there is more than one, we are using a generator # in order to return an enumerator over all the nodes # that were matched, wrapping each of them in turn. return XmlObject(e) for e as XmlElement in elements else: return null # This is to make it easier to work with the node override def ToString(): return _element.InnerText # 它的调用方法如下: print "Using XML OBject" print " - - - - - - - - - " xml = """ <People> <Person> <FirstName>John</FirstName> </Person> <Person> <FirstName>Jane</FirstName> </Person> </People> """ xmlDocument = XmlDocument() xmlDocument.LoadXml(xml) doc = XmlObject(xmlDocument.DocumentElement) for person as XmlObject in doc.Person: print person.FirstName
七、Generators
Boo支持下列三种Generators:
1. Generator Expressions
语法:<expression> for <declarations> [as <type>] in <iterator> [if|unless <condition>]
Generator Expression作为一个enumerator对象使用,所以可以作为函数返回值、函数参数以及赋值给一个变量:
// 作为函数返回值 def GetCompletedTasks(): return t for t in _tasks if t.IsCompleted // 作为变量值 i = 2 a = range(1,5) generator = i*j for j in a print(join(generator)) # prints "2 4 6 8"
2. Generator Methods
Generator方法可以让你有能力自己实现类似Generator Expression的效果,通过在方法中使用yield关键字来建立Generator方法。
例如:
# 你可以自定义实现irange函数(与系统提供的range相似) def irange(begin as int, end as int, step as int): t = begin while t < end: yield t t += step for t in irange(1,5,1): print t
def selectElements(element as XmlElement, tagName as string): for node as XmlNode in element.ChildNodes: if node isa XmlElement and tagName == node.Name: yield node
3. List Generators
提供快速创建List的方法,它有以下多种定义方式:
[element for item in enumerable]
[element for item in enumerable if condition]
[element for item in enumerable unless condition]
List(x**2 for x in range(5)) List(x+2 for x in range(5) if x % 2 == 0)
八、匿名函数/Closures
在Boo中有两种使用Closure的方式:
1、Block based syntax
heater.Notify += def(heater as Heater):
print heater.Temperature
2、Braces based
heater.Notify += {heater as Heater| print heater.Temperature}
a = 0 # declare a new variable getter = { return a } setter = { value | a = value } assert 0 == getter() setter(42) assert 42 == getter() assert 42 == a
【注】:
1、在Boo有这样的语法特性:如果方法参数中某个参数是callable的函数,那么利用上述匿名函数的写法。
例如:
Boo内置函数map将IEnumerable中的每个元素进行指定函数的转换,然后再将结果放入一个新的IEnumerable对象回传回来。
def sqr(x as double): return x*x iter = map(range(0,10),sqr) print join(iter) # 根据刚才的介绍我们也可以有如下更lambda方式的调用: iter1 = map(range(0,10)) def(x as double): return x*x iter2 = map(range(0,10),{x as double | x*x})
2、不定参数
在函数参数args前加*表示参数的数目将是不固定的,类似C#中的params。
// *args表示参数不固定,(object)是类似C#中object[]的表示方法 def Test(*args as (object)): return args.Length print Test(1,2,3,4) a = (2,3) // 注意下面两种调用方式的不同 print Test(a) // 1 print Test(*a) // 2 // Currying // Boo plusX = { a as int | return { b as int | return a + b }} print plusX(3)(4) // C# Func<int,Func<int, int>> plusX = delegate(int x) { return delegate(int y) {return x+y; }; }; Console.WriteLine(plusX(3)(4));
这些特性在我们用Boo来实现DSL中会十分的常见。
九、callable/ICallable
前面我们提到可以将函数赋值给变量,但是这个变量是什么类型呢?我们经常需要把函数当作一个参数传给另一个函数使用。
在Boo中,关键字callable就是将函数作为类型使用(函数指针),与C#中的delegate作用类似。
大家可以在前一篇文章“用Boo实现的Observer”中看到下面两者的含义是一样:
// Boo callable NotifyEventHandler(heater as Heater) // C# public delegate void NotifyEventHandler(Heater heater);
ICallable interface
通常 Boo 允许你调用callable的类型,因此将自己的类实现ICallable接口,你也可以让你的类成为被调用的callable类型。
请看下面的例子:
class Fred(ICallable): def Call(args as (object)) as object: return (args[0] as string) + (args[1] as string) f = Fred() assert f('one','two') == 'onetwo'
参考:
http://tore.vestues.no/category/boo/
http://zh.wikibooks.org/w/index.php?title=BOO/BooFunctions&variant=zh-cn
敬请期待:
循序渐进学Boo - 高级篇
循序渐进学Boo - DSL篇
【附上】
前一阵看到老赵提到的“从CSDN的趣味题学Python”这篇文章,我也用Boo实现了一下(其实与Python极其类似,呵呵,照着改写就好了),权当做熟悉Boo语法。
第一题:
dic = {} for s in "abcdab121": if s not in dic: dic[s] = 1 else: dic[s] = cast(int,dic[s]) + 1 for n in dic.Keys: print "${n}:${dic[n]}"
第二题:
txt = "床前明月光,疑似地上霜,举头望明月,低头思故乡。" offset = 6 b = reversed(i) for i in zip(*array(List(txt[i:i+offset]) for i in range(0,len(txt),offset))) for i in b: print join(i,'|')
从这里大家可以看到Boo和Python在语法特性上十分相似。