hans.hu

夫天地者,万物之逆旅也;光阴者,百代之过客也。而浮生若梦,为欢几何?古人秉烛夜游,良有以也。况阳春召我以烟景,大块假我以文章。

循序渐进学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在语法特性上十分相似。

posted on 2009-04-26 18:52  hans.hu  阅读(2640)  评论(7编辑  收藏  举报

导航