Scala for expression vs. C# LINQ
Do C# ve verzi 3.0 byl přidán LINQ – Language-Integrated Query – způsob, jak deklarativně psát dotazy nad různými zdroji dat pomocí syntaxe vycházející z SQL.
Například následující úryvek kódu vybere ze seznamu čísel všechny, které jsou menší než 3.
from number in list where number < 3 select number
Pro někoho může být překvapivé, že Scala obsahuje velkou část funkcionality LINQu v podobě for expression (někdy také nazývané list comprehension) a kompozice monád. Ve světle těchto zjištění už nemusí být tak překvapivé, že pod kapotou obě technologie pracují celkem podobně.
Scalovská obdoba výše uvedeného fragmentu:
for { number <- list if number < 3 } yield number
Všimněte si, že struktura zcela je totožná, liší se jenom syntaxe a názvy klíčových slov.
For expression, což je obdoba do-notace z Haskellu, dokáže pracovat na jakékoli monádě. Tedy nejen na kolekcích, ale například i užitečné monádě Option[T].
Ve Scale za monádu považujeme jakýkoli objekt, který implementuje metody map
, flatMap
, filter
a případně i foreach
. Přičemž všechny z nich nejsou povinné. Když některé chybí, nerozbije to for expression, ale jenom omezí, co s ním můžeme dělat. Například, když chybí metoda filter
, nelze použít if guard nebo pattern matching; pokud není k dispozici foreach
, for expression musí vracet výsledek (yield) atd.
Ve finále je každý for expression přeložen právě na čtveřici metod map
, flatMap
, filter
a foreach
(viz. kapitola 23.4 Programming in Scala 1 ).
V těle výrazu můžeme použít pattern matching, který zároveň odfiltruje všechny hodnoty, které neodpovídají vzoru na levé straně operátoru <-
.
for (pattern <- expr1 ) yield expr2
Předchozí výraz se přeloží na:
expr1 filter { case pattern => true case _ => false } map { case pattern => expr2 }
Scala nemusí výsledek vracet, ale může vykonat nějaký kód. Jednoduše se místo závěrečného yield uvede blok kódu. Např:
for (i <- list) println(i)
V tomhle srovnání nemůže být řeč o LINQ to SQL, protože jde o kompletní ORM framework, který mapuje řádky databázových tabulek na entity a který navíc využívá vlastnost C#, kdy můžete místo funkce získat jeho stromovou reprezentaci (parse tree), kterou pak LINQ to SQL přeloží na SQL dotaz. Scala tohle neumožňuje, ale plánuje se jiné řešení pomocí polymorphic embeddings (viz & viz), které je sice zaměřeno na DSL a paralelismus, ale dokázalo by vyřešit stejný problém jako LINQ to SQL.
Další řádky se budou týkat jen LINQ to Objects.
LINQ jsou dvě věci: jednak nová syntaxe a pak knihovny. Obojí ušito přesně na míru předpokládanému použití.
Syntaxe je taková, aby co nejvíce připomínala SQL. Podle mě však rozhodně není nutné, aby to byl tak velký zásah do jazyka. Na jedné straně je klasický C# kód, který syntakticky vychází z C a na druhé straně deklarativní konstrukce LINQu. Jsou to dva odlišné světy, které do sebe nezapadají. Deklarativní dotazy v sobě maskují skutečnou podstatu kompozice funkcí.
LINQ, stejně jako for expression, v principu pracuje s monádami (teoreticky), ale je tu jedno ale. Technologie C# si dokáže poradit jenom s třídami, které implementují rozhraní IEnumerable
, což omezuje použití „jenom“ na kolekce. Jenom v uvozovkách, protože vzhledem k předpokládanému use case to není žádný problém.
Zajímavé také je, že LINQ neprovádí žádné výpočty v okamžiku volání příslušných metod. Ty jen zapouzdřují jednotlivé kroky výpočtu, který se provede líně až ve chvíli, kdy je skutečně potřeba a jenom tolik, kolik je ho třeba. Nejde o specialitu bytostně spjatou s LINQem, je to jen záležitost implementace. Scala má také líné datové struktury, např. Stream
.
Dále v LINQu není obdoba metody foreach
a neumožňuje tedy nad výsledkem okamžitě provést nějaké operace a ani to nedává smysl. LINQ je dotazovací jazyk, který vrací data z různých zdrojů. Nemá v popisu práce nad těmito daty provádět nějaké operace kvůli jejich vedlejším účinkům.
Scala znatelně pokulhává v případě (několikanásobného) řazení (order by
) nebo seskupování (group by
). For expression staví čistě na myšlence kompozice monád, které mají jen typový konstruktor a operace „return . (někdy také nazývananou unit)“ a bind – jedna konstruuje monádu, druhá – odpovídající metodě flatMap
– s ní provede nějakou operaci, nic víc. Nepřidává extra konstrukty pro řazení a další SQL-like operace. I když: řazení nebo seskupování se může objevit jenom na konci dotazu, což není takový problém. Navíc group by
fakticky rozdělí LINQ dotaz na dva nesouvisející, které jsou jenom náhodou řazeny za sebou, takže rozdělení na dva for expression vlastně odpovídá realitě.
Ekvivalentní funkce:
C# | Scala |
---|---|
Select | map |
SelectMany | flatMap |
Where | filter |
N/A | foreach |
Ekvivalentní konstrukce:
C# | Scala |
---|---|
from x in y | x ← y |
where x < y | if x < y |
let x = y | x = y |
select x | yield x |
Několik jednoduchých LINQ výrazů ze začátku knihy Essential LINQ 2 a jejich obdoba ve Scale. Některé se přepisují takřka jedna k jedné:
// C# from c in GetCustomers() where c.City == "Mexico" select new { city = c.City, ContactName = c.ContactName }
// Scala for { c <- getCustomers() if c.City == "Mexico" } yield new { val city = c.City, val ContactName = c.ContactName } // nebo tuple: yield (c.City, c.ContactName)
// C# XDocument customers = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement("Customers", new XElement("Customer", new XAttribute("ContactName", "A"), new XAttribute("City", "Berlin")), new XElement("Customer " , new XAttribute("ContactName", "B"), new XAttribute("City", "Mexico")) )); var xml = from x in customers.Descendants("Customer") where x.Attribute("City").Value ≡≡ "Mexico" select x;
// Scala val customers = <Customers> <Customer ContactName="A" City="Berlin" /> <Customer ContactName="B" City="Mexico" /> </Customers> val xml = for { x <- customers \\ "Customer" if (x \ "@City").text == "Mexico" } yield x
Někdy se může hodit mát v jazyce XML literály.
// C# from m in typeof(string).GetMethods() where m.IsStatic == true select m;
// Scala for { m <- classOf[String].getMethods() if java.lang.reflect.Modifier.isStatic(m.getModifiers) } yield m
// C# from m in typeof(string).GetMethods() where m.IsStatic == true orderby m.Name group m by m.Name into g orderby g.Count() select new { Name = g.Key, Overloads = g.Count() };
// Scala val x = for { m <- classOf[String].getMethods() if java.lang.reflect.Modifier.isStatic(m.getModifiers) } yield m val y = x.sortBy(_.getName).groupBy(_.getName) val z = y.map { case (k, v) => (k, v.size) } // nebo val z = for ((k, v) <- y) yield (k, v.size) z.toStream.sortBy { case (k, v) => v }
Tady to není tak úplně fér srovnání. Scala nenabízí obdobu IGrouping, má jenom klasickou třídu Map
, případně SortedMap
, která řadí podle klíčů mapy, ne podle hodnot vypočtených z hodnot mapy.
// C# // list of lists List<int> l1 = new List<int> { 1, 2, 3 }; List<int> l2 = new List<int> { 4, 5, 6 }; List<int> l3 = new List<int> { 7, 8, 9 }; List<List<int>> lists = new List<List<int>> { l1, l2, l3 }; from list in lists from num in list select num;
// Scala val lists = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9)) for { list <- lists num <- list } yield num // nebo neférově lists.flatten
// C# from list in lists from num in list where num % 2 == 0 orderby num descending select num;
// Scala (for { list <- lists num <- list if num % 2 == 0 } yield num).sorted.reverse // nebo (for { ... } yield num).sortWith (_ > _)
// C# var list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; form n in list where (n > 3) & (n < 8) let g = n * 2 let newList = new List<int> { 2, 3 } from l in newList select new { l, r = g * l }
// Scala val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9) for { n <- list if (n > 3) & (n < 8) g = n * 2 newList = List(2, 3) l <- newList } yield (l, g * l)
BTW: Essential LINQ 2 je ze začátku hrozná vymejvárna. Ze začátku není koncipována tak, aby osvětlila co je LINQ, ale pořád dokola opakuje jak je LINQ skvělý a úžasný.
Update: – video LINQ: Language Features for Concurence
- Martin Odersky, Lex Spoon, Bill Venners Programming in Scala Aritma Press, 2007
- Calvert Ch., Kulkarni D. Essential LINQ. Boston, MA: Addison-Wesley, 2009.