[译]Scala DSL教程: 实现一个web框架路由器

yzhi0788 8年前
   <p>原文: <a href="/misc/goto?guid=4959673631919563139" rel="nofollow,noindex">Scala DSL tutorial - writing a web framework router</a> , 作者: Tymon Tobolski</p>    <p>译者按:<br> Scala非常适合实现DSL( <a href="/misc/goto?guid=4959638412616651270" rel="nofollow,noindex">Domain-specific language</a> )。我在使用Scala的过程中印象深刻的是 <a href="/misc/goto?guid=4959673632026191846" rel="nofollow,noindex">scalatest</a> 和 <a href="/misc/goto?guid=4959673632097432096" rel="nofollow,noindex">spray-routing</a> ,</p>    <p>比如scalatest的测试代码的编写:</p>    <pre>  <code class="language-javascript">importcollection.mutable.Stack  importorg.scalatest._    classExampleSpecextendsFlatSpecwithMatchers{    "A Stack"should"pop values in last-in-first-out order"in {  valstack =newStack[Int]   stack.push(1)   stack.push(2)   stack.pop() should be (2)   stack.pop() should be (1)   }     it should "throw NoSuchElementException if an empty stack is popped"in {  valemptyStack =newStack[Int]   a [NoSuchElementException] should be thrownBy {   emptyStack.pop()   }    }  }  </code></pre>    <p>或者 <a href="/misc/goto?guid=4959673632184339272" rel="nofollow,noindex">akka-http</a> 的路由(route)的配置 (akka-http可以看作是spray 2.0的版本,因为作者现在在lightbend,也就是原先的typesafe公司开发akka-http):</p>    <pre>  <code class="language-javascript">valroute =   get {   pathSingleSlash {   complete(HttpEntity(ContentTypes.`text/html(UTF-8)`,"<html><body>Hello world!</body></html>"))   } ~   path("ping") {   complete("PONG!")   } ~   path("crash") {   sys.error("BOOM!")   }   }  </code></pre>    <p>可以看到,使用Scala实现的DSL非常简洁,也符合人类便于阅读的方式。但是我们如何实现自己的DSL呢?文末有几篇参考文档,介绍了使用Scala实现DSL的技术,但是本文翻译的这篇文章,使用Scala实现了一个鸡蛋的web路由DSL,步骤详细,代码简单,所以我特意翻译了一下。以下内容(除了参考文档)是对原文的翻译。</p>    <h3>目标</h3>    <p>Play 2.0的发布给Java社区带来了新的创建web service的方式。尽管非常美好,但是有些组件缺不是我的菜,其中之一它的router定义,它使用定制的route文件,独立的编译器和难以捉摸的逻辑。作为一个Riby程序员,我开始想能否使用Scala实现一个简单的DSL.需求很简单:</p>    <ul>     <li>静态编译</li>     <li>静态类型</li>     <li>易于使用</li>     <li>可扩展</li>     <li>反向路由</li>     <li>尽可能的类型推断</li>     <li>不使用圆括号</li>    </ul>    <h3>设计</h3>    <p>所以第一个问题是:什么是路由器(router)? 它可以表示为 PartialFunction[Request, Handler] ,这就是Play框架中实现的方式。让我们花几秒钟先看看Play的原始的路由器。</p>    <p>在编译的过程中, conf/routes文件下的文件被解析并转换成target/src_managed文件夹下的.scala文件。有两个文件会被产生 routing.scala 和 reverse_routing.scala 。 routing.scala 是一个巨大的 PartialFunction ,每一个路由使用一个case语句。 reverse_routing.scala 对象结构。我真的不喜欢这种方式。</p>    <p>让我们开始探索 <em>如何使用Scala创建一个有用的DSL</em> 。</p>    <p>最终用户ui</p>    <p>我不知道DSL设计的最佳实践,我也从没读过一本关于这方面的书。我用我的方式来实现它。</p>    <p>实现的结果应该自然而直接。首先,描述你想要的,然后实现它。</p>    <p>开始的例子很简单, GET /foo 可以路由到 Application.foo() 方法:</p>    <pre>  <code class="language-javascript">GET "/foo"Application.foo  </code></pre>    <p>这个DSL非常好,但不幸的是,不使用括号的话,无法用Scala按这种方式实现。</p>    <p>当然,你已经知道Scala可是使用 infix notation 和 suffix notation 去掉括号:</p>    <pre>  <code class="language-javascript">A.op(B)  </code></pre>    <p>可以写成</p>    <pre>  <code class="language-javascript">A op B  </code></pre>    <p>同样</p>    <pre>  <code class="language-javascript">A.op(B).opp(C)  </code></pre>    <p>可以写成</p>    <pre>  <code class="language-javascript">A op B opp C  </code></pre>    <p>但是这种写法仅仅适用于只有一个参数的方法, 如 objectA method objectB 。但是在我们上面的DSL例子中(GET "/foo" Application.foo),中间的不是是一个字符串,而不是一个方法名,所以我们不能使用 infix notation 。增加一些中间单词如何:</p>    <pre>  <code class="language-javascript">GET on "/foo"to Application.foo  GET.on("/foo").to(Application.foo)//等价于上面的写法  </code></pre>    <p>编译通过。 GET 可以是一个代表HTTP method的对象, on 是一个方法, /foo 是这个方法的参数,然后 to 是另外一个方法,而 Application.foo 是一个 Function0[Handler] 。 我犯了一个错误,开始去实现它,然后我不得不扔掉了大段代码,因为实现并不能满足我前面定义的需求。</p>    <p>我来把坑挖的更深,来看看路径参数。怎么写一个路由来匹配 GET /foo/{id} 然后调用 Application.show(id) ?,我的初始想法是:</p>    <pre>  <code class="language-javascript">GET on "foo"/ * to Application.show  </code></pre>    <p>看起来很好, / 作为路径分隔符, * 作为参数,而 Application.show 作为 Function1[Int, Handler] 。 / 作为方法实现,而 * 可以作为一个对象,因此上面的语句等价于:</p>    <pre>  <code class="language-javascript">GET.on("foo")./(*).to(Application.show)// 错误!  </code></pre>    <p>事实上, 由于 <a href="/misc/goto?guid=4959673632269498281" rel="nofollow,noindex">Scala操作符优先级的问题</a> ,它实际等价于:</p>    <pre>  <code class="language-javascript">GET.on( "foo"./(*) ).to(Application.show)  </code></pre>    <p>好消息,路径可以组合在一起作为 on 的参数。</p>    <p>更多的例子:</p>    <pre>  <code class="language-javascript">GET on "foo"to Application.foo  PUT on "show"/ * to Application.show  POST on "bar"/ * / * /"blah"/ * to Application.bar  </code></pre>    <p>最后一件事,反向路由(reverse routing)。Play框架默认的路由器有一个限制,一个路由一个action。如果已经定义了一个路由,为什么不把它赋值给val变量用来反向路由呢:</p>    <pre>  <code class="language-javascript">valfoo = GET on"foo"to Application.foo  </code></pre>    <p>然后把路由放在一个对象中:</p>    <pre>  <code class="language-javascript">objectroutes{  valfoo = GET on"foo"to Application.foo  valshow = PUT on"show"/ * to Application.show  valbar = POST on"bar"/ * / * /"blah"/ * to Application.bar  }  </code></pre>    <p>现在可以调用 routes.foo() 或者 routes.show(5) 可以得到路径。</p>    <p>本文的下一部分描述内部实现。现在你可以自己去实现它,或者参考我的实现 <a href="/misc/goto?guid=4959673632345128215" rel="nofollow,noindex">http://github.com/teamon/play-navigator</a> , 但我强烈推荐你继续阅读实现部分。</p>    <h3>实现</h3>    <p>这里有两个难点: type 和 arity 。Scala中的 Function 可以有0到22个参数,代表[Function0]到 Function22 ,后面我会介绍到。</p>    <p>我的实现 play-navigator Route有几个参数:</p>    <ul>     <li>HTTP method</li>     <li>path definition</li>     <li>handler function</li>    </ul>    <p>用下面的例子描述各个部分:</p>    <pre>  <code class="language-javascript">valfoo = GET on"foo"/ * to Application.show  </code></pre>    <p>我们已经知道它等价于:</p>    <pre>  <code class="language-javascript">valfoo = GET.on("foo"./(*) ).to(Application.shows)  </code></pre>    <p>从左边开始,首先 GET 还没有实现,让我们实现它:</p>    <pre>  <code class="language-javascript">sealedtraitMethod  caseobjectANYextendsMethod  caseobjectGETextendsMethod  caseobjectPOSTextendsMethod  </code></pre>    <p>我定义了两个HTTP method和 ANY 对应所有的HTTP method。接下来应该实现 on 方法,但是我们还不知道它使用什么参数。让我们先看看 "foo" / * 。</p>    <p>路径可以有多个变种:</p>    <pre>  <code class="language-javascript">"foo"/"bar"/"baz""foo"/ * /"blah"* / * / *  </code></pre>    <p>幸好路径的各个部分可以用有限的几个类型来表示,它可以是静态路径,也可能是占位符。如此说来,我们可以使用Scala直接实现:</p>    <pre>  <code class="language-javascript">sealedtraitPathElem  caseclassStatic(name: String)extendsPathElem  caseobject*extendsPathElem  </code></pre>    <p>case class 包装了一个字符串,而 * 是一个case object。不幸的的是,因为每个部分都有关联,我不得不描述更多的数据结构。先前我说过Scala有23种不同类型的 Function ,它们有不同数量的参数。我想让类型系统比较 路径占位符的数量和函数参数的数量,如果不匹配就抛出错误。因此我定义了不同版本的 RouteDefN ,我将数量减少到3:</p>    <pre>  <code class="language-javascript">sealedtraitRouteDef[Self]{  defwithMethod(method: Method): Self  defmethod: Method  defelems: List[PathElem]  }    caseclassRouteDef0(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef0]  caseclassRouteDef1(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef1]  caseclassRouteDef2(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef2]  </code></pre>    <p>Self 类型和 withMethod 稍候解释。注意 RouteDefN 并没有类型参数(我说过我想尽可能地在编译的时候检查)。事实是 RouteDefN 仅仅知道它的HTTP method和 path elements,并不会理会handler函数本身。</p>    <p>目前的挑战是如何将</p>    <pre>  <code class="language-javascript">GET on "foo"/ * /"bar"  </code></pre>    <p>转换为</p>    <pre>  <code class="language-javascript">RouteDef1(GET, List(Static("foo"), *, Static("bar")))  </code></pre>    <p>靠隐式函数来救驾了。</p>    <p>首先我们需要将 String 转换成 RouteDef0 :</p>    <pre>  <code class="language-javascript">implicit defstringToRouteDef0(name: String) = RouteDef0(ANY, Static(name) :: Nil)  </code></pre>    <p>任意一个字符串都转换成一个 RouteDef0 ,拥有 ANY method,下一步,同样的技巧应用与 * 类型:</p>    <pre>  <code class="language-javascript">implicit defasterixToRoutePath1(ast: *.type) = RouteDef1(ANY, ast :: Nil)  </code></pre>    <p>之所以是 RouteDef1 是因为已经有一个参数占位符。我们需要实现 / 方法:</p>    <pre>  <code class="language-javascript">caseclassRouteDef0(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef0]{  def/(static: Static) = RouteDef0(method, elems :+ static)  def/(p: PathElem) = RouteDef1(method, elems :+ p)  }    caseclassRouteDef1(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef1]{  def/(static: Static) = RouteDef1(method, elems :+ static)  def/(p: PathElem) = RouteDef2(method, elems :+ p)  }    caseclassRouteDef2(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef2]{  def/(static: Static) = RouteDef2(method, elems :+ static)  }  </code></pre>    <p>/ 方法的逻辑很简单。如果它得到Static参数,那么它返回的类型还是相同的类型。如果得到 * 参数,它返回一个更"高"的路由。 RouteDef2 并不允许传递 * 参数,所以我们没有定义 RouteDef3 。我们还需要实现一个字符串到 Static 的隐式转换。</p>    <pre>  <code class="language-javascript">implicit defstringToStatic(name: String) = Static(name)  </code></pre>    <p>现在我们定义的DSL可以处理:</p>    <pre>  <code class="language-javascript">GET on someRouteDef  </code></pre>    <p>现在是 on 方法如何实现?</p>    <p>让我们返回 Method 定义,它的 on 方法需要类型参数 R ,它会调用routeDef的withMethod方法。</p>    <pre>  <code class="language-javascript">sealedtraitMethod{  defon[R](routeDef: RouteDef[R]): R = routeDef.withMethod(this)  }  </code></pre>    <p>还记得 RouteDef 特质的 withMethod 方法的实现么?</p>    <pre>  <code class="language-javascript">sealedtraitRouteDef[Self]{  defwithMethod(method: Method): Self  }  </code></pre>    <p>现在 RouteDefN 可以写做:</p>    <pre>  <code class="language-javascript">caseclassRouteDef0(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef0]{  defwithMethod(method: Method) = RouteDef0(method, elems)  }    caseclassRouteDef1(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef1]{  defwithMethod(method: Method) = RouteDef1(method, elems)  }    caseclassRouteDef2(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef2]{  defwithMethod(method: Method) = RouteDef2(method, elems)  }  </code></pre>    <p>这样 on 方法就是返回正确的类型。</p>    <p>最后就是和handler拼装起来:</p>    <pre>  <code class="language-javascript">someRouteDef to Application.show  </code></pre>    <p>我说过我想让编译器检查路径参数中的参数数量是否和handler需要的参数数量一致。现在隆重转为疯狂的类 RouteN 出场。</p>    <pre>  <code class="language-javascript">sealed trait Route[RD] {   def routeDef: RouteDef[RD]  }    case class Route0(routeDef: RouteDef0, f0: () ⇒ Out) extends Route[RouteDef0]  case class Route1[A: PathParam : Manifest](routeDef: RouteDef1, f1: (A) ⇒ Out) extends Route[RouteDef1]  case class Route2[A: PathParam : Manifest, B: PathParam : Manifest](routeDef: RouteDef2, f2: (A, B) ⇒ Out) extends Route[RouteDef2]  </code></pre>    <p>呜呼哀哉, 类型、更多的类型、更多坨的类型,保持胃口继续看。 Route0 需要 RouteDef0 和 () ⇒ Out 参数。 Route1 需要 RouteDef1 和 function (A) ⇒ Out ,A为类型参数:</p>    <pre>  <code class="language-javascript">[A: PathParam : Manifest]  </code></pre>    <p>是下面代码的简写:</p>    <pre>  <code class="language-javascript">[A](implicit pp: PathParam[A], mf: Manifest[A])  </code></pre>    <p>PathParam[A] 和 Manifest[A] 稍后解释。</p>    <p>你也可能已经推断出 Route2 使用 RouteDef2 和 function (A,B) ⇒ Out 做参数, A 和 B 都是类型参数。</p>    <p>返回到 RouteDef ,增加 to 方法:</p>    <pre>  <code class="language-javascript">caseclassRouteDef0(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef0]{  defto(f0: () ⇒ Out) = Route0(this, f0)  }    caseclassRouteDef1(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef1]{  defto[A: PathParam : Manifest](f1: (A) ⇒ Out) = Route1(this, f1)  }    caseclassRouteDef2(method: Method, elems: List[PathElem])extendsRouteDef[RouteDef2]{  defto[A: PathParam : Manifest, B: PathParam : Manifest](f2: (A, B) ⇒ Out) = Route2(this, f2)  }  </code></pre>    <p>编译器会检查参数的匹配问题, RouteDefN 的 to 方法只会允许正确的Handler作为参数。</p>    <p>我们可以为 RouteN 增加 def apply 来来检查参数的数量和正确的类型。</p>    <pre>  <code class="language-javascript">case class Route1[A: PathParam : Manifest](routeDef: RouteDef1, f2: (A) ⇒ Out) extends Route[RouteDef1] {   def apply(a: A) = PathMatcher1(routeDef.elems)(a)  }    case class Route2[A: PathParam : Manifest, B: PathParam : Manifest](routeDef: RouteDef2, f2: (A, B) ⇒ Out) extends Route[RouteDef2] {   def apply(a: A, b: B) = PathMatcher2(routeDef.elems)(a, b)  }  </code></pre>    <p>所以如果我们定义了一个路由:</p>    <pre>  <code class="language-javascript">valfoo = GET on"foo"/ * to Application.show  </code></pre>    <p>这里 foo 是一个类型为 Route1[Int](RouteDef1(GET, Static("foo") :: * :: Nil), Application.show) 的对象,同时 foo 还是 (Int) ⇒ String 类型。</p>    <p>关于 PathMatcherN 用来匹配request uri到正确的路由。因为在本文中我只想介绍DSL相关的实现,所以我不想多介绍它。你可以把它看成一个解析和构造url的函数。</p>    <p>现在只剩下一件事。既然所有的路由都是类型安全的,那么我们需要一个类型安全的方式匹配路径和action。一种方式是硬编码,比较傻。既然我们已经有了类型敏感的路由,Scala拥有强大的类型系统,为什么不让工作好上加好呢?</p>    <p>我们需要做什么?</p>    <ul>     <li>解析路径(字符串)为我们的类型</li>     <li>转换路径参数为字符串 (for 反向路由)</li>    </ul>    <p>如何实现呢?</p>    <pre>  <code class="language-javascript">traitPathParam[T]{  defapply(t: T): String  defunapply(s: String): Option[T]  }  </code></pre>    <p>apply 将类型T转换成字符串。而 unapply 将字符串转换成 T 。</p>    <p>下面是两个将路径参数转换成相应类型的例子。</p>    <pre>  <code class="language-javascript">implicit valStringPathParam: PathParam[String] =newPathParam[String] {  defapply(s: String) = s  defunapply(s: String) = Some(s)  }    implicit valBooleanPathParam: PathParam[Boolean] =newPathParam[Boolean] {  defapply(b: Boolean) = b.toString  defunapply(s: String) = s.toLowerCasematch{  case"1"|"true"|"yes"⇒ Some(true)  case"0"|"false"|"no"⇒ Some(false)  case_ ⇒ None   }  }  </code></pre>    <p>因此可以定制类型作为action (handler)的参数。</p>    <p>上文中一个秘密就是RouteN中的PathParam[A],Route类只关心PathParam,所以使用其它类型创建route是不允许的,编译器出错。</p>    <p>Manifest[A]是Scala编译器提供的一个特殊的类,为类型提供运行时的类型信息。</p>    <p>再提供一个java.util.UUID的路径参数:</p>    <pre>  <code class="language-javascript">implicit valUUIDPathParam: PathParam[UUID] =newPathParam[UUID] {  defapply(uuid: UUID) = uuid.toString  defunapply(s: String) =try{   Some(UUID.fromString(s))   } catch{  case_ ⇒ None   }  }  </code></pre>    <p>现在,让我们检查一下我们的需求:</p>    <ul>     <li>静态编译 √</li>     <li>静态类型 √</li>     <li>易于使用 √</li>     <li>可扩展 √</li>     <li>反向路由 √</li>     <li>尽可能的类型推断 √</li>     <li>不使用圆括号 √</li>    </ul>    <p>所有需求都实现。</p>    <p>如果你发现文中有遗漏的地方,或者错误,可以和作者联系 <a href="/misc/goto?guid=4959673632425849907" rel="nofollow,noindex">推ter (@iteamon)</a> , teamon on <a href="/misc/goto?guid=4959673632503898288" rel="nofollow,noindex">#scala @ irc.freenode.net</a> 。</p>    <p>你也可以看完整的项目实现: <a href="/misc/goto?guid=4959673632590548085" rel="nofollow,noindex">play-navigator</a></p>    <p>翻译完毕。</p>    <h3>其它参考资料</h3>    <ol>     <li><a href="/misc/goto?guid=4959673632659241269" rel="nofollow,noindex">DSLs - A powerful Scala feature</a></li>     <li><a href="/misc/goto?guid=4959673632746740620" rel="nofollow,noindex">Creating Domain Specific Languages with Scala - Part 1</a></li>     <li><a href="/misc/goto?guid=4959673632828731586" rel="nofollow,noindex">My First DSL</a></li>     <li><a href="/misc/goto?guid=4959673632904235431" rel="nofollow,noindex">DSLs in Action</a></li>     <li><a href="/misc/goto?guid=4959673632990700658" rel="nofollow,noindex">Writing DSLs using Scala. Part 1 — Underlying Concepts</a></li>     <li><a href="/misc/goto?guid=4959673633066774996" rel="nofollow,noindex">Writing DSLs using Scala. Part II - A simple matcher DSL</a></li>     <li><a href="/misc/goto?guid=4959673633150270698" rel="nofollow,noindex">Domain-Specific Languages in Scala</a></li>     <li><a href="/misc/goto?guid=4959673633232678803" rel="nofollow,noindex">scala-sql-dsl</a></li>    </ol>    <p> </p>    <p>来自: <a href="/misc/goto?guid=4959673633307896189" rel="nofollow">http://colobu.com/2016/05/24/scala-dsl-tutorial-writing-web-framework-router/</a></p>    <p> </p>