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

Scala for expression vs. C# LINQ

13. 2. 2011 (před 8 lety) — k47 (CC by-nc-sa) (♪)

Do C# ve verzi 3.0 byl přidán LINQ – Lan­gu­age-In­te­gra­ted Query – způsob, jak de­kla­ra­tivně psát dotazy nad růz­nými zdroji dat pomocí syn­taxe vy­chá­ze­jící z SQL.


Na­pří­klad ná­sle­du­jící úryvek kódu vybere ze se­znamu čí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ře­kva­pivé, že Scala ob­sa­huje velkou část funk­ci­o­na­lity LINQu v podobě for ex­pres­sion (někdy také na­zý­vané list com­pre­hension) a kom­po­zice monád. Ve světle těchto zjiš­tění už nemusí být tak pře­kva­pivé, že pod ka­po­tou obě tech­no­lo­gie pra­cují celkem po­dobně.

Sca­lov­ská obdoba výše uve­de­ného frag­mentu:

for {
  number <- list
  if number < 3
} yield number

Všim­něte si, že struk­tura zcela je to­tožná, liší se jenom syn­taxe a názvy klí­čo­vých slov.

For ex­pres­sion, což je obdoba do-notace z Haskellu, dokáže pra­co­vat na ja­ké­koli monádě. Tedy nejen na ko­lek­cích, ale na­pří­klad i uži­tečné monádě Option[T].

Ve Scale za monádu po­va­žu­jeme ja­ký­koli objekt, který im­ple­men­tuje metody map, flatMap, filter a pří­padně i foreach. Při­čemž všechny z nich nejsou po­vinné. Když ně­které chybí, ne­roz­bije to for ex­pres­sion, ale jenom omezí, co s ním můžeme dělat. Na­pří­klad, když chybí metoda filter, nelze použít if guard nebo pat­tern matching; pokud není k dis­po­zici foreach, for ex­pres­sion musí vracet vý­sle­dek (yield) atd.

Ve finále je každý for ex­pres­sion pře­lo­žen právě na čtve­řici metod map, flatMap, filterforeach (viz. ka­pi­tola 23.4 Pro­gra­m­ming in Scala 1 ).

V těle výrazu můžeme použít pat­tern matching, který zá­ro­veň od­fil­truje všechny hod­noty, které ne­od­po­ví­dají vzoru na levé straně ope­rá­toru <-.

for (pattern <- expr1 ) yield expr2

Před­chozí výraz se pře­loží na:

expr1 filter {
  case pattern => true
  case _ => false
} map {
  case pattern => expr2
}

Scala nemusí vý­sle­dek vracet, ale může vy­ko­nat nějaký kód. Jed­no­duše se místo zá­vě­reč­ného yield uvede blok kódu. Např:

for (i <- list) println(i)

V tomhle srov­nání nemůže být řeč o LINQ to SQL, pro­tože jde o kom­pletní ORM fra­mework, který mapuje řádky da­ta­bá­zo­vých ta­bu­lek na entity a který navíc vy­u­žívá vlast­nost C#, kdy můžete místo funkce získat jeho stro­mo­vou re­pre­zen­taci (parse tree), kterou pak LINQ to SQL pře­loží na SQL dotaz. Scala tohle ne­u­mož­ňuje, ale plá­nuje se jiné řešení pomocí po­ly­mor­phic em­bed­dings (viz & viz), které je sice za­mě­řeno na DSL a pa­ra­le­lis­mus, ale do­ká­zalo by vy­ře­šit stejný pro­blém jako LINQ to SQL.

Další řádky se budou týkat jen LINQ to Ob­jects.

LINQ jsou dvě věci: jednak nová syn­taxe a pak knihovny. Obojí ušito přesně na míru před­po­klá­da­nému po­u­žití.

Syn­taxe je taková, aby co nej­více při­po­mí­nala SQL. Podle mě však roz­hodně není nutné, aby to byl tak velký zásah do jazyka. Na jedné straně je kla­sický C# kód, který syn­tak­ticky vy­chází z C a na druhé straně de­kla­ra­tivní kon­strukce LINQu. Jsou to dva od­lišné světy, které do sebe ne­za­pa­dají. De­kla­ra­tivní dotazy v sobě mas­kují sku­teč­nou pod­statu kom­po­zice funkcí.

LINQ, stejně jako for ex­pres­sion, v prin­cipu pra­cuje s mo­ná­dami (te­o­re­ticky), ale je tu jedno ale. Tech­no­lo­gie C# si dokáže po­ra­dit jenom s tří­dami, které im­ple­men­tují roz­hraní IEnumerable, což ome­zuje po­u­žití „jenom“ na ko­lekce. Jenom v uvo­zov­kách, pro­tože vzhle­dem k před­po­klá­da­nému use case to není žádný pro­blém.

Za­jí­mavé také je, že LINQ ne­pro­vádí žádné vý­po­čty v oka­mžiku volání pří­sluš­ných metod. Ty jen za­pouz­dřují jed­not­livé kroky vý­po­čtu, který se pro­vede líně až ve chvíli, kdy je sku­tečně po­třeba a jenom tolik, kolik je ho třeba. Nejde o spe­ci­a­litu by­tostně spja­tou s LINQem, je to jen zá­le­ži­tost im­ple­men­tace. Scala má také líné datové struk­tury, např. Stream.

Dále v LINQu není obdoba metody foreach a ne­u­mož­ňuje tedy nad vý­sled­kem oka­mžitě pro­vést nějaké ope­race a ani to nedává smysl. LINQ je do­ta­zo­vací jazyk, který vrací data z růz­ných zdrojů. Nemá v popisu práce nad těmito daty pro­vá­dět nějaké ope­race kvůli jejich ve­d­lej­ším účin­kům.


Scala zna­telně po­kul­hává v pří­padě (ně­ko­li­ka­ná­sob­ného) řazení (order by) nebo se­sku­po­vání (group by). For ex­pres­sion staví čistě na myš­lence kom­po­zice monád, které mají jen typový kon­struk­tor a ope­race „return . (někdy také na­zý­va­na­nou unit)“ a bind – jedna kon­stru­uje monádu, druhá – od­po­ví­da­jící metodě flatMap – s ní pro­vede ně­ja­kou ope­raci, nic víc. Ne­při­dává extra kon­strukty pro řazení a další SQL-like ope­race. I když: řazení nebo se­sku­po­vání se může ob­je­vit jenom na konci dotazu, což není takový pro­blém. Navíc group by fak­ticky roz­dělí LINQ dotaz na dva ne­sou­vi­se­jící, které jsou jenom ná­ho­dou řazeny za sebou, takže roz­dě­lení na dva for ex­pres­sion vlastně od­po­vídá re­a­litě.


Ekvi­va­lentní funkce:

C#Scala
Selectmap
Se­lectManyflatMap
Wherefilter
N/Afo­re­ach

Ekvi­va­lentní kon­strukce:

C#Scala
from x in yx ← y
where x < yif x < y
let x = yx = y
select xyield x

Ně­ko­lik jed­no­du­chých LINQ výrazů ze za­čátku knihy Es­sen­tial LINQ 2 a jejich obdoba ve Scale. Ně­které se pře­pi­sují 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 li­te­rá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 srov­nání. Scala ne­na­bízí obdobu IGrou­ping, má jenom kla­sic­kou třídu Map, pří­padně SortedMap, která řadí podle klíčů mapy, ne podle hodnot vy­počte­ný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: Es­sen­tial LINQ 2 je ze za­čátku hrozná vy­mej­várna. Ze za­čátku není kon­ci­po­vána tak, aby osvět­lila co je LINQ, ale pořád dokola opa­kuje jak je LINQ skvělý a úžasný.


Update: – video LINQ: Lan­gu­age Fe­a­tu­res for Con­cu­rence


  1. Martin Ode­r­sky, Lex Spoon, Bill Ven­ners Pro­gra­m­ming in Scala Aritma Press, 2007
  2. Cal­vert Ch., Kul­karni D. Es­sen­tial LINQ. Boston, MA: Ad­dison-Wesley, 2009.
píše k47 & hosté, ascii@k47.cz