k47.cz    — každý den dokud se vám to nezačne líbit
foto Praha výběr povídky kultura | twitter FB


Scala for expression vs. C# LINQ

13. 2. 2011 — k47

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


  1. Martin Odersky, Lex Spoon, Bill Venners Programming in Scala Aritma Press, 2007
  2. Calvert Ch., Kulkarni D. Essential LINQ. Boston, MA: Addison-Wesley, 2009.

vstoupit do diskuze    sdílet na facebooku, twitteru, google+

příbuzné články:
ScalaQuery 📷
Scala - typově bezpečné eventy 📷
Scala type bounds vs. C# constraints on type parameters 📷
Scala - přednášky 📷
Scala versus C# 4.0 - Strukturální typy versus Dynamic member lookup 📷
Scala - trait Dynamic 📷

sem odkazují:
Další várka článků převážně o Scale

píše k47 & hosté, ascii@k47.cz