0xDEADBEEF

RSS odkazy english edition
««« »»»

Proč nemám rád Go

25. 10. 2020

Důvod je velice jednoduchý: Protože v něm nemůžu přímo vyjádřit to, co chci vyjádřit. Vždy musím skákat skrz obruče a dělat to způsobem, který nikdy nevede nejjasnější cestou k cíli.

Hlavní příčinou je chybějící parametrický polymorfismus, v méně civilizovaných kruzích známý jako generika. Bez něho není snadné vytvářet mocné, ale přesto bezpečné abstrakce. Nemám na mysli divoké veletoče type level programování a monadické transformátory, ale něco mnohem jednoduššího jako jsou metody map a filter --- udělej stejnou věc s objekty různých typů.

K téhle litanii mě rozhoupal článek o tom, jak je C++ hrozně nepřehledné a Go je mnohem lepší. Demonstroval to na programu v C++, který pak přepsal do Go. C++ je hrozný zmatek, to ano, Go je o něco málo lepší, ale ne o moc. Stále je v něm příšerně moc šumu a nepotřebných odboček. Připomíná to přirozený jazyk, jehož mluvčí se nikdy nedostane za úroveň pátého ročníku a chybí mu tak slovník pro efektivní pojmenovávání komplexních myšlenek a musí je tak donekonečna opisovat.

Přepsal jsem ukázku do Scaly a až na pár drobností je koncentrace kroků vedoucích za cílem k počtu nutných konstrukcí skoro perfektní. Jako v dobrém scénáři, kde každé slovo posouvá děj k rozuzlení, i tady každý token směřuje jasně k cíli, popisuje, co chci udělat a ne nutné repetitivní detaily, aby byl bůh kompilátorů spokojený. To je ve staticky typovaném jazyce možné právě díky parametrickému polymorfismu, kvůli němuž do sebe můžeme různé komponenty bezpečně zapojit.

import scala.collection.mutable
import scala.jdk.StreamConverters._
import java.nio.file._

val wordRegex  = "(?i)([a-z]{2,})".r
val wordCounts = mutable.Map[String, Int]().withDefaultValue(0)

for {
  f <- Files.walk(Paths.get(".")).toScala(Iterator)
  if Files.isRegularFile(f) && f.toString.endsWith(".txt")
  str = io.Source.fromFile(f.toFile, "utf-8").mkString
  word <- wordRegex.findAllIn(str.toLowerCase)
} wordCounts(word) += 1

wordCounts.toSeq.sortBy { case (k, v) => ~v }.take(10).foreach(println)

Ok, je tu pár podivností jako .toScala(Iterator) pro překlenutí propasti mezi Java a Scala API nebo => ~v pro sestupné řazení podle intu. Ty ale nejsou nutné, jde jen o nejpřímější způsoby, jak udělat danou věc. Pokud bych chtěl něco očividnějšího pro netrénované pozorovatele, mám tu možnost, můžu to zapsat ve formě smyček a ifů. Na druhou stranu v jazycích bez parametrického polymorfismu, nemám možnost vytvořit a používat tyto abstrakce a primitivní pinožení je to nejlepší, v co můžeme doufat.

Napsal jsem to ještě v jazyce D, jehož raison d'être je být příčetné C++ a zase: kód je krátký, přímý, čitelný a k věci.

Téměř každá funkce je ve skutečnosti template, což není úplně to samé co parametrický polymorfismus, ale poskytuje stejně efektivní a flexibilní API, které dělá to, co chcete nehledě na přesný typ argumentu. Když zavolám array, vytvoří to pole z argumentu nehledě co je zač a jakého typu jsou elementy. Když má metody pro procházení, výsledkem bude pole.

import std.file;
import std.regex;
import std.string;
import std.array;
import std.algorithm;
import std.stdio;

void main() {
  auto wordRegex = regex("([a-z]{2,})", "i");
  int[string] wordCounts;

  foreach (f; dirEntries(".", "*.txt", SpanMode.depth)) {
    if (!isFile(f)) continue;
    auto str = readText(f).toLower;

    foreach (m; matchAll(str, wordRegex)) {
      wordCounts[m[0]]++;
    }
  }

  auto wordsArray = wordCounts.byKeyValue.array;
  auto top10 = topN!"a.value > b.value"(wordsArray, 10);

  foreach (pair; top10) {
    writeln(pair.key, " ", pair.value);
  }
}

To je všechno, co jsem chtěl říct. Parametrický polymorfismus je klíč k expresivitě, stručnosti a eleganci.

píše k47 (@kaja47, k47)