Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

DSLs in Scala & DB4O

2,217 views

Published on

A presentation about DSLs in general, what Scala has to offer in terms of internal DSLs and finally a custom DSL example that uses the DB4O database.

Published in: Technology, Business
  • Be the first to comment

DSLs in Scala & DB4O

  1. 1. [1] Scala Scala present DSLs in Scala for MSI at HSMA Marcus Körner (@atla_) Johannes Wachter (@jow85)
  2. 2. 2 Grande Cinnamon Dolce Latte with tripple shot with non-fat milk topped with whipped cream [2]
  3. 3. 3 [3] place orders ( new Order to buy(100 sharesOf ”IBM”) limitPrice 300 allOrNone using premiumPricing, new Order to sell(200 bondsOf ”CISCO”) limitPrice 300 allOrNone using { (qty, unit) => qty * unit - 500 } ) Beispiel aus [DSLSINACTION]
  4. 4. 4 Agenda Was sind Warum DSLs Scala? DSL Beispiele DB4O DSL in Scala
  5. 5. [4] Was sind DSLs?
  6. 6. 6 DSL Allgemeine Definition ˃ Domain Specific Language • Oder auch Fluent API ˃ Domain – [Domäne] • Problemorientiert • „Fachsprache“ ˃ Specific – [Speziell] • Konkret entworfen für einen spezifischen Einsatzzweck • Abgrenzung zur General Purpose Language
  7. 7. 7 Domäne … oder auch Anwendungsdomäne ˃ Sammlung an Abläufen, Objekten und Rahmenbedingungen die Teil der Fachdomäne sind ˃ Brücke zwischen realen Objekten aus der Welt des Anwenders und Objekten eines Softwaresystems ˃ Domain-Driven-Design • Schwerpunkt ist die Fachlichkeit und Fachlogik • Ideales Einsatzgebiet für DSLs!
  8. 8. 8 Ausdrucksschwäche von GPLs Ocean ocean = new Ocean (); Fish fish1 = new Fish (); fish1.setSize(Size.TINY); fish1.setColor(Color.RED); Fish fish2 = new Fish (); fish2.setSize(Size.MIDSIZE); fish2.setColor(Color.BLUE); Shark shark = new Shark (); shark.setSize(Size.HUGE); shark.setColor(Color.WHITE); Jellyfish jellyfish = new Jellyfish (); jellyfish.setSize(Size.SMALL); Turtle turtle = new Turtle (); turtle.setSize (Size.SMALL); ocean.add (fish1); ocean.add (fish2); ocean.add (shark); ocean.add (jellyfish); ocean.add (turtle);
  9. 9. 9 Ausdrucksschwäche von GPLs Ocean ocean = new Ocean (); Wenn man eigentlich meint… Fish fish1 = new Fish (); ocean ( fish1.setSize(Size.TINY); ? fish1.setColor(Color.RED); Fish fish2 = new Fish (); fish2.setSize(Size.MIDSIZE); fish (TINY, RED), fish (SMALL, BLUE), shark (HUGE, WHITE), ? fish2.setColor(Color.BLUE); jellyfish (SMALL), Shark shark = new Shark (); turtle (SMALL) shark.setSize(Size.HUGE); ) shark.setColor(Color.WHITE); ? Jellyfish jellyfish = new Jellyfish (); jellyfish.setSize(Size.SMALL); Turtle turtle = new Turtle (); turtle.setSize (Size.SMALL); ? ocean.add (fish1); ocean.add (fish2); ocean.add (shark); ocean.add (jellyfish); ocean.add (turtle);
  10. 10. 10 Ausdrucksschwäche von GPLs Ocean ocean = new Ocean (); Mit Scala 2.8: Named Parameters Fish fish1 = new Fish (); ocean ( fish1.setSize(Size.TINY); ? fish1.setColor(Color.RED); Fish fish2 = new Fish (); fish2.setSize(Size.MIDSIZE); ) fish (size=TINY, color=RED), jellyfish (size=SMALL) ? fish2.setColor(Color.BLUE); Shark shark = new Shark (); shark.setSize(Size.HUGE); shark.setColor(Color.WHITE); ? Jellyfish jellyfish = new Jellyfish (); jellyfish.setSize(Size.SMALL); Turtle turtle = new Turtle (); turtle.setSize (Size.SMALL); ? ocean.add (fish1); ocean.add (fish2); ocean.add (shark); ocean.add (jellyfish); ocean.add (turtle);
  11. 11. 11 [5] Klassifizierung von DSLs
  12. 12. 12 Klassifizierung ˃ Es gibt zwei wesentliche Unterschiede bei DSLs • Interne DSLs • Externe DSLs ˃ Es gibt allerdings auch einige graphische DSLs
  13. 13. 13 Interne DSLs ˃ Eingebettet in eine Host-Sprache • z. B. in Java, Groovy, Scala, Python, C# … ˃ Beeinflusst und limitiert von den Sprachmitteln der Host-Sprache ˃ Einige Vertreter • LINQ (C#), Grails (Groovy), Lift (Scala), WebDSL (Scala)
  14. 14. 14 Externe DSLs ˃ Liegen meist in Form von Skripten oder interpretierbaren Text-Dateien vor • Graphviz, (X)HTML ˃ Werden entweder • interpretiert • compiliert in eine andere Sprache (Xtext) ˃ Kennen wir alle nur zu gut… • SQL, Ant-Skripte, Makefiles, XML Konfigurationen
  15. 15. 15 Graphische DSLs ˃ Graphische Modellierung einer Domäne • UML, BPM ˃ Meist verbunden mit einem spezifischen Tool • Bsp. Microsoft Oslo, JetBrains MPS, Intentional Domain Workbench ˃ Gut geeignet zum Dokumentieren ˃ Schlecht geeignet zum eigentlichen Entwickeln!
  16. 16. 16 [6] Stand der Dinge
  17. 17. 17 Naive API … ohne wirkliches Design Cream c = new WhippedCream (); Coffee coffee = new [7] Coffee(”CinnamonDolce”, TYPE_LATTE); coffee.sized(4); coffee.setDecaf (”decaf none”); coffee.addCream(c);
  18. 18. 18 Query API … am Starbucks Beispiel Coffee coffee = new Coffee(); coffee.setSize(Size.Grande); coffee.setType(Type.CinnamonDolceLatte); coffee.setDecaf(DecafLimit.Full); coffee.setMilk(Milk.NonFat); coffee.setCream(Cream.WhippedCream);
  19. 19. 19 Query API … am Starbucks Beispiel Coffee coffee = new Coffee(); coffee.setSize(Size.Grande); coffee.setType(Type.CinnamonDolceLatte); coffee.setDecaf(DecafLimit.Full); coffee.setMilk(Milk.NonFat); coffee.setCream(Cream.WhippedCream); ˃ Das geht doch schöner, oder?
  20. 20. [8] Builder-Pattern
  21. 21. 21 Das Builder-Pattern … Expression-Builder ˃ Grundlage • Mittels Method-Chaining Lesbarkeit von Query APIs erhöhen ˃ Umsetzung • Ein oder mehrere Objekte die ein Fluent Interface anbieten und in eine darunterliegende Query-API transformieren. ˃ Ziel • Soll ähnlich lesbar wie eine interne DSL sein • Gutes Design für moderne APIs: Builder für die wichtigsten Objekte
  22. 22. 22 Allgemeine Umsetzung … In Java Object o = new Object.Builder(General) .configureA (A) .configureB (B) .configureC (C) .build (); [9]
  23. 23. 23 Fluent-APIs (mit dem Builder-Pattern) … am Starbucks Beispiel Coffee coffee = new Coffee.Builder (Size.Grande, Type.CinnamonDolceLatte) [10] .with (DecafLimit.Half) .with (Milk.Soy) .with (Cream.WhippedCream) .build();
  24. 24. 24 Fluent-APIs (mit dem Builder-Pattern) … am Starbucks Beispiel Coffee coffee = new Coffee.Builder (Size.Grande, Type.CinnamonDolceLatte) [10] .with (DecafLimit.Half) .with (Milk.Soy) .with (Cream.WhippedCream) .build(); ˃ Nicht schlecht, aber warum soviel Overhead?
  25. 25. 25 Code is written for people … and only then for computers! [11]
  26. 26. 26 [12] Ziele für DSLs
  27. 27. 27 Ziele Im Vordergrund ˃ Klare Wiedergabe der Absicht eines Systems • Im Bezug auf die Domäne ˃ Einfachere Lesbarkeit • flüssig • Hin zu natürlicher Sprache ˃ Bessere Modellierung der Domäne ˃ Domänenexperten sollten DSL verstehen und lesen können • Nicht zwingend selbst damit arbeiten (Wunschdenken)
  28. 28. 28 Ziele … langfristig ˃ Abstraktionsebene von Software erhöhen ˃ Software-Entwicklung erleichtern ˃ Zeit einsparen • Bei der Kommunikation mit Domänen-Experten • Beim Umsetzen von fachlichen Anforderungen
  29. 29. 29 Ziele … langfristig ˃ Abstraktionsebene von Software erhöhen ˃ Software-Entwicklung erleichtern ˃ Zeit einsparen • Bei der Kommunikation mit Domänen-Experten • Beim Umsetzen von fachlichen Anforderungen ˃ Und nein, Entwickler wollen wir nicht abschaffen • Hat bei COBOL auch nicht geklappt ;)
  30. 30. 30 Schwierigkeiten … bei der Umsetzung ˃ Korrekte Abbildung einer Domäne in einer gegebenen Sprache oftmals schwierig ˃ Anpassung an die Host-Sprache notwendig ˃ DSL fühlt sich „fremd“ an in der Host-Sprache ˃ Warum passt sich die Sprache nicht der Domäne an?
  31. 31. 31 Roadmap für den Einsatz von DSLs … am Beispiel Scala ˃ Schritt 1 • Tests von Java Objekten mit Scala DSL ˃ Schritt 2 • Scala DSL als Smart-Wrapper um Java Objekte herum ˃ Schritt 3 • Nicht-kritische Funktionalität mit Hilfe einer Scala DSL modellieren ˃ Man muss nicht gleich Produktionscode auf Scala umstellen…
  32. 32. 32 [13] Warum Scala?
  33. 33. 33 Warum Scala … einsetzen für Domain Specific Languages? ˃ Weniger „Noise“ • Optionale Punkte beim Methodenaufruf • Semikolon-Inferenz • Typ-Inferenz • Operatoren als Methoden und Infix-Operatoren • Optionale Klammern (an manchen Stellen) ˃ Funktionale Aspekte • Gleich mehr dazu ˃ Combinator Parsing (für externe DSLs)
  34. 34. 34 „Fluent“-ness von Scala Ohne val s : Int = new Palm().get (new Banana(3)).size;
  35. 35. 35 „Fluent“-ness von Scala mit Companion-Object und Type-Inference val s : Int = new Palm().get (new Banana(3)).size; val s = Palm().get (Banana(3)).size;
  36. 36. 36 „Fluent“-ness von Scala mit Punkt- und Semicolon-Inferenz val s : Int = new Palm().get (new Banana(3)).size; val s = Palm().get (Banana(3)).size; val s = Palm() get (Banana(3)) size
  37. 37. [14] CLOSURES & FUNCTIONS
  38. 38. 38 CLOSURES & FUNCTIONS Closures ˃ Anonyme Funktionen ˃ Verwendung von Variablen aus dem aktuellen Scope möglich val greeting = "Welcome" val greet : String => String = s => { greeting + " " + s } println greet("MSI Course") // Welcome MSI Course
  39. 39. 39 CLOSURES & FUNCTIONS Currying (1/2) def uncurried(s:String, i:Int):String = { s + i } println(uncurried("foo -> ", 1)) // foo -> 1
  40. 40. 40 CLOSURES & FUNCTIONS Currying (2/2) def curried(s:String)(i:Int):String = { s + " " + i } val prefill = curried("foo") _ Currying ermöglicht val execute = prefill(1) - Partially Applied Functions println(execute) // foo 1 - „Neue“ Kontrollstrukturen val control = curried("foo"){ 1 + 10 - 12 } println(control) // foo -1
  41. 41. 41 CLOSURES & FUNCTIONS Implicit Conversions Best Practice Implicit Conversions in ˃ „Magische Konvertierung“ Singleton Objects kapseln. ˃ Vorhandene APIs erweitern import some.Object.* class EnhancedInt(val int:Int){ def toBinaryString():String = { // ... } } implicit def intToEnhancedInt(i:Int):EnhancedInt={ new EnhancedInt(i) } println(12 toBinaryString) // 1100
  42. 42. [15] CASE CLASSES
  43. 43. 43 CASE CLASSES Idee ˃ Normale Klassen mit Modifier case ˃ Erhalten implizit erweiterte Funktionen • Verwendbarkeit in Pattern Matching • Implizites Companion Object • Automatische Vergleichbarkeit
  44. 44. 44 CASE CLASSES Definition und Verwendung abstract class TrainWaggon case class StandardWaggon(seats : Int) extends TrainWaggon case class BistroWaggon(seats : Int, bar : Boolean) extends TrainWaggon case class BaggageWaggon(capacity : Int) extends TrainWaggon StandardWaggon(50) BistroWaggon(20, true) BaggageWaggon(200)
  45. 45. 45 CASE CLASSES Beispiel „Option“ val result : Option[String] = ... // Ergebnis einer anderen Funktion Some("Inhalt") // Es wurde ein Ergebnis erstellt und zurückgeliefert None // Es wurde kein Ergebnis erstellt
  46. 46. [17] PATTERN MATCHING
  47. 47. 47 PATTERN MATCHING Matching auf Werte val input : String = "..." // Aus Eingaben, Dateien, ... val choice = input match { case "xml" => exportToXML case "json" => exportToJSON case "doc" => exportToWord case "docx" if isDocxEnabled => exportToWord case _ => invalidExport // Weitere Eingabenwerte }
  48. 48. 48 PATTERN MATCHING Matching mit Objekten ˃ Case Classes optimal verwendbar ˃ Elegante Steuerung des Kontrollflusses val input : Option[String] = ... // Some("test"), None input match { case Some("foobar") => false case Some("test") => true case None => false }
  49. 49. [18] PARTIAL FUNCTIONS
  50. 50. 50 PARTIAL FUNCTIONS val match1 : PartialFunction[String, String] = { case "pictures" => displayPictureGallery case "about" => displayAboutInformation } val match2 : PartialFunction[String, String] = { case "users" => displayUserList case "news" => displayNews } match1("users") match1 isDefinedAt "users" // scala.MatchError // false match2("users") match2 isDefinedAt "users„ // führt displayUserList aus // true
  51. 51. [19] PARSER COMBINATORS
  52. 52. 52 PARSER COMBINATORS [MAGIC] object SimpleScala extends RegexpParsers { val ID = """[a-zA-Z]([a-zA-Z0-9]|_[a-zA-Z0-9])*"""r val NUM = """[1-9][0-9]*"""r def program = clazz* // ... def classPrefix = "class" ~ ID ~ "(" ~ formals ~ ")" // ... Parser direkt in Scala anhand einer DSL def expr: Parser[Expr] = factor ~ ( definieren. "+" ~ factor | "-" ~ factor Beispiele: )* - JSON - SimpleScala // ... - WebDSL
  53. 53. [20] Beispiele für DSLs in Scala
  54. 54. [21] Finance DSL in Scala
  55. 55. 55 Finance DSL in Scala aus DSLsinAction val fixedIncomeTrade = 200.discount_bonds(IBM) .for_client(NOMURA) .on(NYSE) .at(72.ccy(USD)) Beispiel aus [DSLSINACTION]
  56. 56. 56 Finance DSL in Scala aus DSLsinAction val fixedIncomeTrade = 200 discount_bonds IBM for_client NOMURA on NYSE at 72.ccy(USD) Beispiel aus [DSLSINACTION]
  57. 57. [22] Starbucks DSL in Scala
  58. 58. 58 Starbucks Scala DSL Beispiel für die Verwendung val o = order ( Tall (CinnamonDolceLatte decaf None withMilk NonFat withCream WhippedCream), Grande (ConPanna decaf Half), Venti (FlavoredLatte decaf None withMilk Soy withCream NoWhippedCream) )
  59. 59. 59 Starbucks Scala DSL Schade ist… val o = order ( Tall (CinnamonDolceLatte decaf None withMilk NonFat withCream WhippedCream), Grande (ConPanna decaf Half), Venti (FlavoredLatte decaf None withMilk Soy withCream NoWhippedCream) )
  60. 60. 60 Starbucks Scala DSL Verwendete Konzepte ˃ Companion + apply () für order (…) ˃ Case Objects um „new“ zu entgehen • Kaffeesorten (CinnamonDolceLatte, ConPanna…) • Milchsorten (NonFat, Soy…) • Größen (Tall, Venti, Grande…)
  61. 61. [23] ScalaTest DSL
  62. 62. 62 ScalaTest DSL http://www.scalatest.org class StackSpec extends FlatSpec with ShouldMatchers { "A Stack" should "pop values in last-in-first-out order" in { val stack = new Stack[Int] stack.push(1) stack.push(2) stack.pop() should equal (2) stack.pop() should equal (1) } it should "throw NoSuchElementException if an empty stack is popped" in { val emptyStack = new Stack[String] evaluating { emptyStack.pop() } should produce [NoSuchElementException] } } Beispiel von [SCALATEST]
  63. 63. 63 Und mehr … weitere Scala DSLs ˃ ScalaModules [SCALAMODULES] • Konfiguration von OSGi Bundles ˃ Squeryl [SQUERYL] • Scala ORM und DSL für SQL-Datenbanken ˃ Baysick [BAYSICK] • Scala DSL die BASIC implementiert ˃ Apache Camel DSL [CAMELDSL] • DSL zur Integration von ESB Komponenten
  64. 64. [24] DB4O [LOGOS]
  65. 65. 65 DB4O Native Queries ˃ Interfaces: Predicate<T>, Comparator<T> final ObjectSet<User> nativeQuery = database.query(new Predicate<User>() { @Override public boolean match(final User o) { final User user = o; return user.getLastName().equals("Flanders") && user.getFirstName().endsWith("od"); } });
  66. 66. 66 DB4O [SCADB4O] [SCADB4O2] Offensichtliche Vereinfachung für Native Queries final ObjectSet<User> nativeQuery = database.query(new Predicate<User>() { … }); ˃ Anonyme Klasse ersetzen durch Closure ˃ Transformation mit Implicit Conversions db query { user : User => user.name.contains("t") } implicit def toPredicate[T](predicate: T => Boolean) = { new Predicate[T]() { def `match`(entry: T): Boolean = { predicate(entry) Durch fehlende } Kompatibilität mit } Query Optimizer nicht } „Production ready“
  67. 67. 67 DB4O SODA Queries ˃ Builder-Pattern final Query query = database.query(); query.constrain(User.class); query.descend("firstName").constrain("Johannes"); query.descend("lastName").constrain("Wachter"); query.descend("birthday").constrain("1985").like(); query.descend("age").constrain("19").smaller(); final ObjectSet<User> execute = query.execute();
  68. 68. 68 DB4O Komplexere DSL für SODA Queries (1/8) Idee DB4O Abfragen SQL-ähnlich modellieren!
  69. 69. 69 DB4O Komplexere DSL für SODA Queries (2/8) db select User.getClass where(„name := “Bart“) and('age < 20)
  70. 70. 70 DB4O Komplexere DSL für SODA Queries (3/8) ObjectContainer db select User.getClass where(„name := “Bart“) and('age < 20) implicit def oCToDSLOC(c : ObjectContainer):DSLObjectContainer = DSLObjectContainer(c) case class DSLObjectContainer(val c : ObjectContainer){…}
  71. 71. 71 DB4O Komplexere DSL für SODA Queries (4/8) db select User.getClass where(„name := “Bart“) and('age < 20) case class DSLQuery[T](query : Query, clazz : Class[T]) extends QueryUtil{ def where(constr : DSLConstraint):ExtendedDSLQuery[T]={…} def order(order : DSLOrdering):DSLQuery[T]{…} def execute()={…} }
  72. 72. 72 DB4O Komplexere DSL für SODA Queries (5/8) def :=(obj:Any):DSLConstraint={…} Symbol db select User.getClass where(„name := “Bart“) and('age < 20) implicit def sToConstr(s: Symbol):DSLConstraint={ DSLConstraint(symbol) }
  73. 73. 73 DB4O Komplexere DSL für SODA Queries (6/8) abstract class Operator „name := “Bart“ case object SMALLER extends Operator case object SMALLER_EQUAL extends Operator case class DSLConstraint(s : Symbol, var b : Any = 0, var op : Operator = EQUALS){ def :=(obj:Any):DSLConstraint={…} def ~|(obj:String):DSLConstraint={…} }
  74. 74. 74 DB4O Komplexere DSL für SODA Queries (7/8) val res = c.operator match { case EQUALS => q.descend(c.symbol.name) .constrain(c.bound).equal case SMALLER => q.descend(c.symbol.name) .constrain(c.bound).smaller case SMALLER_EQUAL => q.descend(c.symbol.name).constrain(c.bound).smaller() .or(q.descend(c.symbol.name).constrain(c.bound).equal) } def or(c : DSLConstraint):ExtendedDSLQuery[T]={ q.constraints().or(constrain(q, c)) this }
  75. 75. 75 DB4O Komplexere DSL für SODA Queries (8/8) ˃ Verwendete Scala Features • Implicit Conversions • Builder Pattern • Case Objects • Pattern Matching ˃ Typisches Beispiel: Wrapper für Bibliothek ˃ Viele Möglichkeiten APIs „lesbar“ abzubilden
  76. 76. [25] THE DAWN OF DSLs [LOGOS]
  77. 77. 77 [26] DEMO
  78. 78. SOURCECODE http://github.com/jwachter/scala-db4o-dsl
  79. 79. 79 [27] NOCH FRAGEN?
  80. 80. 80 Werbung http://scala-southerngermany.mixxt.de/
  81. 81. 81 Quellen [BAYSICK] http://blog.fogus.me/2009/03/26/baysick-a-scala-dsl-implementing-basic/ [STIME] http://github.com/jorgeortiz85/scala-time [SCADB4O] http://matlik.net/blog/2007/11/28/scala-and-db4o-native-queries/ [SCADB4O2] http://www.matthewtodd.info/?p=68 [SQUERYL] http://squeryl.org/ [SCALATEST] http://www.scalatest.org [DSLSINACTION] Debashish Ghosh – DSLs In Action (MEAP) (http://manning.com/ghosh/) [MAGIC] Daniel Spiewak, http://www.codecommit.com/blog/scala/the-magic-behind-parser-combinators [FOWLER] Martin Fowler, http://martinfowler.com/dslwip (News zum Buch unter http://martinfowler.com/snips/201005261418.html) [SCALAMODULES] http://wiki.github.com/weiglewilczek/scalamodules/ [CAMELDSL] http://camel.apache.org/scala-dsl.html
  82. 82. 82 Quellen Bilder (1/3) [1] XKCD, http://xkcd.com/353/ [2] http://www.flickr.com/photos/stephenccwu/3035854901/ [3] http://www.flickr.com/photos/travel_aficionado/2396814840/ [4] http://www.flickr.com/photos/leonardlow/340763653/ [5] http://www.flickr.com/photos/13698839@N00/3001363490/ [6] http://www.flickr.com/photos/spherical_perceptions/4634722058/ [7] http://www.flickr.com/photos/7202153@N03/4623364964/ [8] http://www.flickr.com/photos/v1ctory_1s_m1ne/3416173688/ [9] http://www.flickr.com/photos/gorbould/3083371867/ [10] http://www.flickr.com/photos/jpdaigle/4221047282/ [11] http://www.flickr.com/photos/trinity-of-one/20562069/ [12] http://www.flickr.com/photos/bogdansuditu/2377844553/ [13] http://www.flickr.com/photos/stuartmckenna/3104554689/ [14] http://www.flickr.com/photos/erikcharlton/421678891/
  83. 83. 83 Quellen Bilder (2/3) [15] http://www.flickr.com/photos/zkorb/1592677291/ [17] http://www.flickr.com/photos/mortimer/221051561/ [18] http://www.flickr.com/photos/flying_cloud/2666399483/ [19] http://www.flickr.com/photos/fattytuna/8586848/ [20] selbst fotografiert (Buch: Programming in Scala) [21] http://www.flickr.com/photos/travel_aficionado/2396824478/ [22] http://www.flickr.com/photos/webel/2406479887/ [23] http://www.flickr.com/photos/whisperwolf/3487084290/ [24] http://www.flickr.com/photos/adesigna/3237575990/ [25] http://www.flickr.com/photos/mugley/4207122005/ [26] http://www.flickr.com/photos/jof/263652571/ [27] http://www.flickr.com/photos/themonnie/2500388784/
  84. 84. 84 Quellen Bilder(3/3) [LOGOS] DB4O - http://www.db4o.com LIFT - http://www.liftweb.net WEBDSL - http://www.webdsl.org Squeryl - http://max-l.github.com/Squeryl ScalaTest - http://scalatest.org Grails - http://www.grails.org Scala - http://www.scala-lang.org Groovy - http://groovy.codehaus.org Ruby - http://ruby-lang.org Python – http://www.python.org
  85. 85. 85 License Präsentation http://creativecommons.org/licenses/by-nc-nd/3.0/de/ Quelltext http://www.apache.org/licenses/LICENSE-2.0.html

×