Advent of Code 2024 – 25 Sprachen (German)

Alexander Schoch,programmingpythongerman

Dieses Jahr habe ich für Advent of Code versucht, etwas Neues zu probieren (zumindest für mich): Ich wollte versuchen, jeden Tag das Rätsel mit einer anderen Programmiersprache zu lösen.

Dazu habe ich 25 Sprachen gesammelt, die ich generell benutzen/lernen würde, sie gemischt und die für den jeweiligen Tag vorgesehene Sprache verwendet (also keine freie Wahl).

In diesem Blogbeitrag werde ich über die Erfahrungen mit jeder Sprache sprechen und 0-5 Sterne für

vergeben.

Bedenke, dass dies sehr subjektiv ist und meine Erfahrung mit vielen dieser Sprachen eher gering ist. Ausserdem sind funktionale Programmiersprachen und Low-Level-Sprachen in der Regel weniger nützlich für Advent of Code. Ich habe Sprachen ausgewählt, die

🎄

Was ist Advent of Code?

Advent of Code ist eine von Eric Wastl erstellte Website, die 25 Tage lang Rätsel aufgibt. Diese Rätsel beinhalten immer eine kurze Geschichte über das Problem, das die Protagonisten lösen müssen, und zwei Aufgaben: Eine Leichtere und eine Schwierigere. Jedes Rätsel wird ausführlich anhand eines Beispiels erklärt und gibt einen personalisierten, sehr grossen Input, der mit einer Programmiersprache deiner Wahl zu einem korrekten Resultat führen soll.

Python

Erfahrung:
Spass:

Python ist ein Klassiker und wahrscheinlich eine der einfachsten Sprachen, um AoC-Probleme zu lösen. Durch die Verwendung von pandas für I/O und numpy für Datenverarbeitung ist Python sehr praktisch, um diese Probleme zu lösen.

Alles in allem war es mit Python ziemlich einfach, aber es ist keine allzu wilde Sprache.

import numpy as np
import pandas as pd
 
data = pd.read_csv('input.txt', delimiter='\t', header=None)
 
col1 = data[0].sort_values().values
col2 = data[1].sort_values().values
 
a = np.absolute(col1 - col2)
 
print('Part 1:', sum(a))
 
### part 2
 
s = 0
 
for i in col1:
    ii = np.where(col2 == i)[0]
    s += i * len(ii)
 
print('Part 2:', s)

Rust

Erfahrung:
Spass:

Ich habe viel Gutes über Rust gehört, und da ich von C/C++ komme, gibt es viele Dinge, die mein Leben komfortabler machen könnten, während die Sprache immer noch Low-Level ist. Beispiele dafür sind foreach-Schleifen, eine nicht-beschissene Art und Weise der Ausgabe von Sachen nach stdout, explizite Mutability, praktisches Tooling (Cargo), und (looking at you, C++) eine konsistente Syntax. Insgesamt: Das hat Spass gemacht!

use std::fs::File;
use std::io::{BufRead, BufReader};
 
fn main() {
    let mut sum = 0;
 
    let file = File::open("input.txt").unwrap();
    let reader = BufReader::new(file);
 
    for (_index, line) in reader.lines().enumerate() {
        let line = line.unwrap();
 
        let parts = line.split(" ").collect::<Vec<_>>(); 
 
        let mut fin = false;
 
        // iterate parts.len()..parts.len()+1 for Part 1
        for j in 0..parts.len()+1 {
            let mut parts_red = parts.clone();
 

Scala

Erfahrung:
Spass:

In der halben Stunde, die ich mit Scala verbracht habe, konnte ich feststellen, dass diese Sprache mit etwas JavaScript-Erfahrung sehr leicht zu erlernen ist. Ausserdem kompiliert Scala zu Java-Bytecode und transpiliert zu JavaScript, so dass Scala wirklich cool sein kann, wenn die Seltsamkeiten von JavaScript oder der “Classes down your throat”-Ansatz von Java unattraktiv sind.

Die Dokumentation ist solide, und der Einstieg in Scala ist smooth. Ich finds toll.

//> using scala 3.5.2
 
val part1 = false
 
@main
def hello(): Unit =
  val lines = scala.io.Source.fromFile("input.txt").mkString
 
  val regex = "mul\\(\\d+,\\d+\\)".r
  val dorx = "do\\(\\)".r
  val dontrx = "don't\\(\\)".r
 
  val m = Vector() ++ regex.findAllMatchIn(lines)
  val mind = regex.findAllMatchIn(lines).map(_.start).toList
  val doind = dorx.findAllMatchIn(lines).map(_.start).toList
  val dontind = dontrx.findAllMatchIn(lines).map(_.start).toList
 
  val products = m
    .map(i => i.toString)
    .zipWithIndex

PHP

Erfahrung:
Spass:

Ich habe viel PHP mit dem Symfony Web-Framework geschrieben, und ich habe PHP immer als eine Sprache für das Web geschätzt, die einfach zu schreiben ist. Allerdings ist das Debuggen von PHP ein Albtraum, und diese Erfahrung habe ich auch hier gemacht. PHP ist wirklich gut darin, einfach anzunehmen, was man tun will, selbst wenn es falsch ist. Was ich allerdings an PHP mag, ist, dass es viele wirklich praktische Funktionen eingebaut hat. Die Dokumentation ist allerdings mühsam zu lesen.

<?php
 
function main() {
  $file = 'input.txt';
 
  if (!file_exists($file)) {
    return;
  }
 
  $lines = array();
 
  foreach(file($file) as $line) {
    array_push(
      $lines,
      substr($line, 0, strlen($line)-1)
    );
  }
 
  $hits = 0;
 

D

Erfahrung:
Spass:

Wenn diese Sprache wie C aussieht, liegt das daran, dass sie das auch soll. Das Schreiben in D fühlt sich an wie das Schreiben von C mit einigen praktischen Dingen auf aus C++ und der Abstraktion der “komplizierten” Dinge aus C. Features, die D beinhaltet: Den string Datentypen, dynamische Arrays, praktische string Funktionen, etc.

Wenn man von C/C++ kommt, ist D ziemlich schnell erlernbar.

import std.stdio;
import std.array;
import std.conv;
import std.file;
import std.algorithm;
 
void main()
{
  string file = cast(string)std.file.read("input.txt");
  string[] input = readLines(file);
 
  Appender!(string[]) rules_app;
  Appender!(string[]) updates_app;
 
  bool weInRulesSection = true;
 
  for (int i = 0; i < input.length; i++) {
    string ln = input[i];
 
    if(ln == "") {

Go

Erfahrung:
Spass:

Go ist toll. Es hat mich nicht genervt, die Tour of Go ist wirklich praktisch für den Einstieg in Go, das Tooling ist gut, der Code ist lesbar und schnell. Go fühlt sich wie die logische Schlussfolgerung für eine Low-Level Sprache an, die von C inspiriert ist.

Ein Beispiel von Go-Syntax, das ich mag, ist das C statement

int x = 5;

das in Go

var x int = 5

wäre. Dies sortiert Information nach Relevanz und liest sich sehr natürlich (eine Variablen mit dem Namen x, die ein int ist und den Wert 5 trägt).

package main
 
import (
  "bufio"
  "fmt"
  "os"
)
 
func main() {
  file, _ := os.Open("input.txt")
  defer file.Close()
 
  scanner := bufio.NewScanner(file)
 
  var input [][]string
  var index int = 0
 
  for scanner.Scan() {
    var ln string = scanner.Text()
    var lnarr []string

Elixir

Erfahrung:
Spass:

Ich habe ein wenig Elixir geschrieben, als ich das Phoenix-Webframework ausprobiert habe, und die Leute haben online für diese Sprache geworben, also habe ich sie miteinbezogen. Offenbar fühlt man sich schlau, wenn man Elixir schreibt, und die Sprache sei sehr elegant.

Ich hatte nicht allzu viel Mühe, den Code für die AoC-Aufgabe zu schreiben. Aber der Code ist hässlich! Schauen wir uns zum Beispiel Zeile 8 an. Ich möchte eine Zeichenkette bei : aufteilen, das erste Element nehmen und es als Ganzzahl behandeln. In JavaScript wäre das const res = Number(ln.split(': ')[0]), aber Elixir gibt mir zwei Möglichkeiten, das erste Element zu nehmen: Entweder verwende ich Elem.at(), was die Zeile unnötig lang macht, oder ich verwende pattern matching, das eine weitere Codezeile hinzufügt.

In Anbetracht dieses kleinen Ärgernisses macht es mir Spass, Code in Elixir zu schreiben, und die Dokumentation ist a) gut genug für Anfänger, und b) super mit etwas Erfahrung im Erlang-Ökosystem.

{:ok, input} = File.read("input.txt")
 
operators = {"+", "*", "||"}
 
lines = input |> String.split("\n") |> Enum.filter(fn ln -> ln != "" end)
s = lines |> Enum.map(
  fn ln -> 
    res = ln |> String.split(": ") |> Enum.at(0) |> Integer.parse() |> elem(0)
    cont = ln |> String.split(": ") |> Enum.at(1) |> String.split(" ") |> Enum.map(fn num -> Integer.parse(num) |> elem(0) end)
 
    exp = (cont |> length) - 1
    combinations = 3**exp
 
    # Try each combination of operators
    matches = 1..combinations |> Enum.map(fn i -> i - 1 end) |> Enum.map(fn i -> 
      bin = i |> Integer.to_string(3) |> String.pad_leading(exp, "0")
 
      # merge them together to an eval string
      # parts = 1..String.length(bin) |> Enum.map(fn i -> i - 1 end) |> Enum.map(fn i -> 
      #   j = bin |> String.at(i) |> Integer.parse() |> elem(0)

Java

Erfahrung:
Spass:

Ich mein, es ist Java. Ich mag es nicht besonders, aber es macht mir auch nichts aus. Die Dokumentation ist allerdings recht mühsam. Wie im Code sichtbar ist, bin ich kein grosser Freund des objektorientierten Ansatzes (es sei denn, das Problem passt gut zu diesem Ansatz), also habe ich eine einzige Klasse Main verwendet, die alle Methoden enthält, die ich brauche.

public class Main {
  public static void main(String... args) {
    BufferedReader reader;
 
    List<String> input_al = new ArrayList<String>();
 
    try {
      reader = new BufferedReader(new FileReader("input.txt"));
      String line = reader.readLine();
 
      while (line != null) {
        input_al.add(line);
        line = reader.readLine();
      }
 
      reader.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
 

Julia

Erfahrung:
Spass:

Julia ist ein interessanter Fall. Es fühlt sich ein bisschen wie Python an und zeichnet sich durch dieselbe Art von Tasks aus, für die man Python/NumPy verwenden würde, hat aber bessere Performance.

Das Julia Getting Started ist sehr hilfreich, aber die Suche nach Informationen über die Verwendung bestimmter Funktionen in den Dokumenten kann mühsam sein. Ausserdem finde ich, dass Julia-Code dazu neigt, ziemlich eintönig auszusehen, was die Orientierung innerhalb der Datei manchmal schwierig macht.

Probiere Julia aus, wenn du Python magst und etwas Ähnliches ausprobieren möchtest (oder Dinge wie δ = 5 machen willst).

module AOC
  input = read("input.txt", String)
 
  blocks = []
 
  print("Generating Blocks...\n")
  for i = firstindex(input):lastindex(input)-1
 
    ch = input[i] 
    chnum = parse(Int64, ch)
 
    if i % 2 == 1
      for j = 1:chnum
        push!(blocks, div(i, 2))
      end
    else
      for j = 1:chnum
        push!(blocks, -1)
      end
    end

Lisp Nim

Erfahrung:
Spass:

Ich habe mich zunächst für Lisp entschieden, genauer gesagt für Common Lisp. Nach drei Stunden, in denen ich versucht habe, die Sprache zu lernen, bin ich nicht wirklich weiter als ein einfaches “Hello World” hinausgekommen und habe daher beschlossen, Lisp durch Nim zu ersetzen.

Die Sprache mag zwar cool sein, aber der Einstieg in Lisp ist unglaublich mühsam und nichts, was man an einem zufälligen Dienstag tun sollte, um ein AoC-Puzzle zu lösen. Der Hauptgrund dafür ist die Inexistenz einer brauchbaren Dokumentation und eines “Common-Lisp-Crashkurses”, der nicht aus einem 1500-Seiten-Buch besteht.

(format t "Scheiss auf Lisp, ich benutze Nim.")
Erfahrung:
Spass:

Nim fühlt sich sehr wie Python mit Datentypen an. Es ist ziemlich einfach, Code zu schreiben, getting started ist effizient, aber die Dokumentation kann ziemlich kryptisch mit sehr wenig Beschreibung aussehen. Ein grosser Teil des Grundes ist wahrscheinlich die recht kleine Community.

Nim Docs Example

import strutils
import sequtils
 
let inputfile = readFile("input.txt")
 
var inp: seq[seq[int]]
 
for ln in splitLines(inputfile):
  if(ln == ""):
    continue
  var line: seq[int]
  for ch in toSeq(ln.items):
    line.add(parseInt(ch & ""))
  inp.add(line)
 
proc is_valid(pos: array[0..1, int], input: seq[seq[int]], val: int): bool =
  if pos[0] < 0:
    return false
  if pos[0] >= len(input):
    return false

Dart

Erfahrung:
Spass:

Das Schlimmste an Dart war für mich, den Interpreter ohne den ganzen Ballast zu installieren, der normalerweise dazugehört (eine vollständige IDE wie Android Studio zum Beispiel).

Meine Erfahrung mit Dart war in Ordnung, und ich sehe die Programmierung plattformübergreifender Apps damit, aber JavaScript gibt mir in der Regel mehr Werkzeuge, um das zu tun, das ich brauche. Fazit: Eh.

import 'dart:io';
 
void main() {
  File file = File("input_smol.txt");
  var fileContent = file.readAsStringSync();
 
  var inputStr = fileContent.replaceAll("\n", "").split(" ");
  var inp = <int>[];
 
  for(var i = 0; i < inputStr.length; i++) {
    inp.add(int.parse(inputStr[i]));
  }
 
  int s = 0;
  for(var k = 0; k < inp.length; k++) {
    var input = [inp[k]];
    for(var i = 0; i < 30; i++) {
      var len = input.length;
      for(var j = 0; j < len; j++) {
        int num = input[j];

Pascal

Erfahrung:
Spass:

Ich wollte zunächst Delphi benutzen, aber ich fand heraus, dass es Closed Source und kostenpflichtig war. So entschied ich mich stattdessen für Delphi’s Daddy.

Pascal ist eine ziemlich alte Sprache, und ich war ein wenig besorgt darüber, in welchem schlechten Zustand sie sein könnte. Ein paar Stunden später habe ich echt Spass mit Pascal. Diese Sprache ist ungefähr so alt wie meine Eltern, aber Software in Pascal zu schreiben ist ehrlich gesagt ziemlich elegant!

Pascal ist ein wenig einzigartig in der Art, wie Programme strukturiert sind. Zum Beispiel finden alle Variablendeklarationen am Anfang eines Blocks statt, Pascal unterscheidet zwischen procedure und function, usw.

program AOC12;
 
uses
  Classes, SysUtils;
type
  inptype = array of array of char;
  coords = array of int64;
  posarray = array of coords;
const
  directions: array of array of int64 = ((1, 0), (0,1), (-1,0), (0,-1));
...
  repeat
    N := length(S);
    if j = 0 then
      setLength(inp, N, N);
 
    for i:=0 to length(S)-1 do
    begin
      inp[j][i] := S[i+1];
    end;

Gleam

Erfahrung:
Spass:

Gleam fühlt sich wie eine sehr moderne funktionale Sprache an, was mir sehr gut gefällt. Die Gleam Language Tour ist super hilfreich und zeigt auch die fortgeschrittenen Features ziemlich zügig. Allerdings, ähm, schau dir den Code unten an.

Bei der Besprechung von Elixir habe ich bereits auf den Boilerplate der Array-Operationen angespielt. Gleam treibt dies auf die nächste Stufe, indem es versucht, perfekt im Error Handling zu sein.

Man stelle sich vor: Viele Funktionen von gleam/list geben nicht nur einen Wert zurück, sondern auch einen Statuscode, z.B. Ok([1, 2, 3]). Das bedeutet, dass man den Wert einer Funktion durch die Verwendung von gleam/result/unwrap oder durch die Verwendung von use (was der beabsichtigte Weg ist) erhalten kann. Diese sehr einfachen Ketten von String-Operationen würden dann in etwa 5 Zeilen aufgeteilt werden, und ich würde dafür lieber nur eine einzige Zeile verwenden.

Ein cooles Feature wäre es, diese Funktionen sowohl als “Error-Prone” als auch als “Simple” anzubieten, z.B. list.first(), das Ok(3) zurückgeben könnte, und list.first_(), das 3 zurückgeben könnte, ähnlich wie das Node.js fs Modul mit seinen ...Sync Funktionen.

pub fn main() {
  let assert Ok(inp) = sf.read(from: filepath)  
 
  let prices = inp |> str.split("\n\n") |> list.map(fn(s) {
    let parts = s |> str.split("\n") |> list.filter(fn(st) { st != "" })
 
    let ax = parts |> list.first() |> res.unwrap("") |> str.split("X+") |> list.last() |> res.unwrap("") |> str.split(",") |> list.first() |> res.unwrap("") |> int.parse() |> res.unwrap(0)
    let ay = parts |> list.first() |> res.unwrap("") |> str.split("Y+") |> list.last() |> res.unwrap("") |> int.parse() |> res.unwrap(0)
 
    let bx = parts |> list.drop(1) |> list.first() |> res.unwrap("") |> str.split("X+") |> list.last() |> res.unwrap("") |> str.split(",") |> list.first() |> res.unwrap("") |> int.parse() |> res.unwrap(0)
    let by = parts |> list.drop(1) |> list.first() |> res.unwrap("") |> str.split("Y+") |> list.last() |> res.unwrap("") |> int.parse() |> res.unwrap(0)
 
    let pxtmp = parts |> list.last() |> res.unwrap("") |> str.split("X=") |> list.last() |> res.unwrap("") |> str.split(",") |> list.first() |> res.unwrap("") |> int.parse() |> res.unwrap(0)
    let pytmp = parts |> list.last() |> res.unwrap("") |> str.split("Y=") |> list.last() |> res.unwrap("") |> int.parse() |> res.unwrap(0)
 
    let px = prefix + pxtmp
    let py = prefix + pytmp
 
    let invdet = 1.0 /. {ax * by - ay * bx |> int.to_float()}
    let x = invdet *. {by * px |> int.to_float()} -. invdet *. {bx * py |> int.to_float()}

Haskell

Erfahrung:
Spass:

Das war harzig. Ich fange an, die zunehmende Schwierigkeit von AoC zu bemerken, und Haskell ist hier nicht hilfreich.

Ich habe schon einige funktionale Programmiersprachen benutzt, aber keine von ihnen zwingt mich nicht nur dazu, Code so zu schreiben, wie der Compiler es will. Nein, man braucht auch ein ganzes Philosophiestudium, nur um zu verstehen, was genau das Keyword do genau macht.

Ich bin mir sicher, dass Haskell ein guter Weg ist, um funktionales Programmieren zu lernen, aber für den Zweck, die Sprache kurz für AoC zu lernen, ist Haskell einfach… schlecht.

main :: IO ()
main = do
  let filename = "input_smol.txt"
  inputStr <- readFile filename
  let input = inputStr & splitOn "\n" & filter (\s -> s /= "")
 
  let startPos = map (\s -> splitOn " " s & head & splitOn "=" & last & splitOn "," & map stringToInt) input
  let vel = map (\s -> splitOn " " s & last & splitOn "=" & last & splitOn "," & map stringToInt) input
 
  let endPos = zip startPos vel & map Main.iterate
 
  let tl = endPos & filter (\p -> (head p) < (div nx 2) && (last p) < (div ny 2)) & length
  let tr = endPos & filter (\p -> (head p) > (div (nx-1) 2) && (last p) < (div ny 2)) & length
  let bl = endPos & filter (\p -> (head p) < (div nx 2) && (last p) > (div (ny-1) 2)) & length
  let br = endPos & filter (\p -> (head p) > (div (nx-1) 2) && (last p) > (div (ny-1) 2)) & length
 
  print(endPos)
 
iterate :: ([Int], [Int]) -> [Int]
iterate d = 

Fortran

Erfahrung:
Spass:

Wenn man bedenkt, dass Fortran ursprünglich eine Sprache für Lochkarten war, hat diese Sprache einen langen Weg hinter sich. Stele dir eine Sprache vor, die ähnlich wie C ist, aber mit allen Annehmlichkeiten von NumPy. So sieht modernes Fortran aus.

Abgesehen vom Einlesen der Eingabedaten war das Schreiben des Codes in Fortran sehr angenehm. Eine solide Sprache, die ich für high perfomance computing wieder verwenden würde (viel cooler als C++).

  character, dimension(NY,2*NX) :: input
  character, dimension(NC) :: instructions
 
  open(unit=read_unit, file=filename, iostat=ios)
  if ( ios /= 0 ) stop "Error opening file input_tinee.txt"
 
  k = 1
 
  do i = 1, NY+NL
    read(read_unit,*,iostat=ios) ctmp
    if(.not. bottom) then
      do j = 1, NX
        input(i, 2*j-1) = ctmp(j:j)
        input(i, 2*j) = ctmp(j:j)
        if(ctmp(j:j) == "O") then
          input(i, 2*j-1) = "["
          input(i, 2*j) = "]"
        end if 
        if(ctmp(j:j) == "@") then
          pos = [i, 2*j-1]

TypeScript

Erfahrung:
Spass:

Ich benutze TypeScript fast täglich, und ich denke, dass es eine grossartige Sprache ist, um AoC-Rätsel schnell zu lösen. Erstens erlaubt sie konventionelle (mutability und side effects) und funktionale Programmierung zur gleichen Zeit, und npm hat eine riesige Anzahl von Paketen, um fast jede Aufgabe zu lösen (z.B. das astar-typescript-Paket für Pfadfindung). Ausserdem hat mir das Ausprobieren einer Reihe von Sprachen mit Datentypen in den letzten zwei Wochen gezeigt, wie wahnsinnig “schlau” TypeScript’s Typprüfung tatsächlich ist (z.B. hat Dart meinen Type Cast einfach ignoriert).

Ich mag TS, es macht Spass.

function main() {
  const inputStr = readFileSync("input.txt", 'utf8');
  const input = inputStr
    .split('\n')
    .filter(e => e !== "")
    .map(
      e => e.split("")
      .filter(i => i !== "")
    );
 
  let start, end;
  for(let i = 0; i < input.length; i++) {
    for(let j = 0; j < input[0].length; j++) {
      if(input[i][j] === "S")
        start = [i, j];
      else if (input[i][j] === "E")
        end = [i, j];
    }
  }
 

Bash

Erfahrung:
Spass:

Bash ist super. Für mich ist es das ultimative Beispiel für eine Skill, der in Minuten zu erlernen ist und wirklich schnell Stunden spart. Wenn man bedenkt, dass Bash eine Sprache ist, mit der man innerhalb von 3 Minuten etwas zusammenhacken kann, bin ich beeindruckt, wie gut sich Bash eigentlich für das Lösen von AoC-Rätseln eignet.

Ich mag es, dass ich mit Bash einfach Code schreiben kann, ohne darüber nachzudenken, was mein Code eigentlich unter der Haube macht. Bei den meisten high-level Sprachen muss man sich manchmal noch Gedanken über Memory machen. In Bash? Bash macht einfach seinen Scheiss, ohne Fragen zu stellen. Zum Guten oder zum Schlechten.

Ein grosser negativer Punkt an Bash ist, dass es ab etwa 100 Zeilen Code, der Schleifen und Bedingungen enthält, absolut unlesbar wird.

  instrcode="${INSTRUCTIONS[$PTR]}" 
  skip=false
 
  case $instrcode in
    0) # adv
      pow=$((2 ** $comboper))
      A=$(($A / $pow))
      ;;
    1) # bxl
      B=$(($B ^ $litoper))
      ;;
    2) # bst
      B=$(($comboper % 8))
      ;;
    3) # jnz
      if [[ $A -ne 0 ]]
      then
        PTR=$litoper
        echo "PTR: $PTR -- INSTR: $instrcode, OPER: $litoper, A: $A, B: $B, C: $C"
        #skip=true

C

Erfahrung:
Spass:

Bisher habe ich nur C++, aber nie reines C benutzt. Es war also eine schöne Abwechslung, einmal nur gcc zu benutzen.

Was allerdings nicht so schön war, war die Übung, für die ich C verwenden sollte: Pathfinding (Dijkstra), das normalerweise stark auf dynamische Arrays (std::vector in C++, aber kein Äquivalent in C) angewiesen ist. Meine Freunde lachten mich sogar aus, als ich ihnen erzählte, dass ich Dijkstra in C programmieren würde, und das zu Recht.

Im Allgemeinen finde ich C ziemlich einfach zu schreiben. Ich vermisse einige grundlegende C++-Funktionen, wie iostream, fstream und die Übergabe von Variablen per Referenz anstelle ihres Pointers (unter der Hood ist es dasselbe, aber der Code wird lesbarer).

Natürlich hatte ich, wie jeder nicht super erfahrene C-Programmierer, meinen Anteil an Segfaults (etwa 20, wenn ich mich richtig erinnere), typischerweise weil ich vergessen hatte, einen Pointer in einer Funktion zu derefenzieren.

Insgesamt war das Schreiben von AoC in C in Ordnung, aber ich vermisse einige der praktischen Funktionen von higher-level Sprachen. Ich habe C zwei “Spass”-Sterne gegeben, weil C einfach nur C ist, nichts allzu Verrücktes oder Neues daran.

  for(int i = 0; i < N; i++) {
    for(int j = 0; j < N; j++) {
      if(input[i][j] == '.') {
        allNodes[numNodes] = getId(i, j);
        numNodes++;
      } 
      count_u[getId(i, j)] = 0;
    }
  }
 
  // the queue of nodes to test
  B[0][0] = getId(start[0], start[1]);
  Bcosts[0] = 0;
  Blengths[0] = 1;
  Bind++;
 
  int iterations = 0;
 
  while(Bind > -1 && count_u[getId(end[0], end[1])] < k) {
    // find lowest cost in B

Perl

Erfahrung:
Spass:

Ich muss sagen, dass ich das nicht erwartet habe. Auf den ersten Blick sieht Perl aus, als hätten PHP und Bash ein Kind bekommen, was ich bei einer Programmiersprache nicht erwarten würde.

Davon abgesehen, wenn du RegEx magst, wirst du Perl lieben. Es fühlt sich an wie eine Sprache, die sich hervorragend für schnell zusammengebastelte Skripte eignet, die Text verarbeiten (was, wenn man offene Formate und die Kommandozeile verwendet, so ziemlich alles ist). Sie hat ein paar lustige Design-Entscheidungen, wie z.B. das Schlüsselwort my, um eine Variable zu deklarieren.

Eine sehr interessante Eigenheit von Perl ist, dass Arrays mit einem @ und Skalare (d.h. einzelne Werte) mit einem $ beginnen. Dieses Präfix ist nicht Teil des Variablennamens und kann sich ändern, wenn sich die Operation ändert. Zum Beispiel ist dies ein vollkommen valides Perl:

my $val = $array_of_numbers[5];
my @vals = @array_of_numbers[2..5];

Da ein Element eines Arrays ein Skalar ist, wird $ verwendet. Für ein Slice eines Arrays, das wiederum ein Array ist, wird @ verwendet.

use Memoize qw(memoize);
 
sub say {print @_, "\n"}
 
open(my $in, "<", "input.txt") or die "Can't open file: $!";
 
my @input = <$in>;
 
my @patterns = map { s/\n//r } split(/, /, $input[0]);
my @designs = map { s/\n//r } @input[2..$#input];
 
#say "Patterns: " . join(", ", @patterns);
 
memoize('assign_patterns');
 
my $s = 0;
foreach (@designs) {
  my $combinations = assign_patterns($_);
  say $_ . ": " . $combinations;
  $s += $combinations;

Erlang

Erfahrung:
Spass:

Ich habe schon einiges über Erlang und sein Ökosystem gehört, da ich bereits einige Elixir-Programme entwickelt habe. Ich hatte erwartet, dass sich Erlang ähnlich wie Elixir verhalten würde. Ich habe schnell gelernt, dass dies nicht der Fall ist.

Mein erster Eindruck von Erlang war eigentlich ganz gut. Ich mochte, dass jede Funktion mit einem . endet und jede Zeile mit einem , abgeschlossen wird, ähnlich wie ein Satz in Deutsch. Erlang war eine sehr frühe funktionale Programmiersprache (1987), also war ich froh, typische funktionale Muster wie map oder filter zu sehen. Der export am Anfang ist ganz nett, da man so einen Überblick über alle Funktionen in einer Datei hat, ohne sie überfliegen zu müssen.

Und dann bin ich in die Seltsamkeiten von Erlang hineingeraten. Zunächst einmal muss eine Variable immer mit einem Grossbuchstaben beginnen. Wenn man das nicht tut, bekommt man eine sehr kryptische Fehlermeldung:

Eshell V14.2.5.5 (press Ctrl+G to abort, type help(). for help)
1> A = 5.
5
2> a = 5.
** exception error: no match of right hand side value 5

Der Grund dafür ist, dass ein Identifier in Kleinbuchstaben eigentlich ein Atom ist, das wie eine konstante Variable ist, die ihren eigenen Namen als Wert enthält. Einem Atom kann man nichts zuweisen, also kann 5 nicht auf a matchen. Diese Fehlermeldung macht Sinn, wenn man sich mit diesen Dingen auskennt, aber die meisten Anfänger (wie ich) tun das nicht.

Eines der schönen Dinge an modernen funktionalen Programmiersprachen ist, dass man Pipelines erstellen kann, um mehrere Operationen in einer einzigen Codezeile auszuführen, ähnlich wie die Pipes von Bash. Elixir und Gleam haben den |> Operator, Haskell hat &, JavaScript hat . und Erlang hat… nichts. Das führt zu einer grausamen Verschachtelung von Funktionen oder dazu, dass jede Kleinigkeit ihren eigenen Variablennamen braucht. Der Grund für den fehlenden Pipe-Operator ist offenbar, dass Erlang in der Reihenfolge verschiedener Argumenttypen nicht konsistent ist.

Eine weitere Seltsamkeit ist das Fehlen eines string/char-Typs in Erlang. Text wird als eine Liste von binary gespeichert, und die Erlang-Shell interpretiert einen Wert, wie sie es für richtig hält. Das führt zu komischem Verhalten wie diesem:

1> [86, 87, 88].
"VWX"

In Erlang gibt es keine Schleifen (wohl aber lists:map). Um eine “while”- oder “for”-Schleife zu erstellen, erstellt man eine rekursive Funktion:

% if `Count` is 10, the function call will match the top version.
loop(10) ->
  ok;
loop(Count) ->
  % do something
  loop(Count+1).

Ich mag diesen Ansatz zwar, aber was wirklich nervt, ist, dass er a) ohne Memoisierung extrem langsam sein kann und b) dass jede Schleife eine eigene Funktion benötigt, was zu einer grossen Codefragmentierung führt.

Nachdem ich mehrere Stunden damit verbracht hatte, Dijkstras Algorithmus in Erlang zu schreiben, habe ich es tatsächlich geschafft, ihn zum Laufen zu bringen, aber er war wahnsinnig langsam. Es war nicht einfach möglich, die Dijkstra-Funktion zu memoisieren, also löste ich die Herausforderung stattdessen mit TypeScript und einem A* npm-Paket.

-module(main).
-export([main/0, readlines/1, dijkstra/3, dijkstra_loop/5, elem/2, getCoords/2, getId/2, getNeighbors/3, updateNeighs/6, getDist/3, print/1, getIndexOf/2, removeWall/2, removeWallLn/2, getPath/3]).
 
main() ->
  Data = readlines("input_smol.txt"),
  SRow = lists:nth(1, lists:filter(fun(I) -> lists:member(<<"S">>, lists:nth(I, Data)) end, lists:seq(1, length(Data)))),
  SCol = lists:nth(1, lists:filter(fun(I) -> lists:nth(I, lists:nth(SRow, Data)) == <<"S">> end, lists:seq(1, length(lists:nth(SRow, Data))))),
 
  ERow = lists:nth(1, lists:filter(fun(I) -> lists:member(<<"E">>, lists:nth(I, Data)) end, lists:seq(1, length(Data)))),
  ECol = lists:nth(1, lists:filter(fun(I) -> lists:nth(I, lists:nth(ERow, Data)) == <<"E">> end, lists:seq(1, length(lists:nth(ERow, Data))))),
 
  X = length(Data),
  Y = length(lists:nth(1, Data)),
  Walls = lists:filter(fun(Ele) -> elem(Data, getCoords(Data, Ele)) == <<"#">> end, lists:seq(1, X * Y)),
  _WallsCoords = lists:map(fun(Ele) -> getCoords(Data, Ele) end, Walls),
  
  {_BDist, BPrev, BAll} = dijkstra(Data, [SRow, SCol], [ERow, ECol]),
  print(BPrev),
  BPath = getPath(BPrev, getId(Data, [ERow, ECol]), BAll),
  _BC = length(BPath)-1,

Ruby

Erfahrung:
Spass:

Ich habe ein wenig Ruby verwendet, als ich vor ein paar Jahren Ruby on Rails ausprobierte. Für dieses AoC-Rätsel fand ich Ruby extrem intuitiv zu schreiben. Schreibe einfach Code, bei dem du denkst, dass er in Ruby korrekt sein könnte, und es funktioniert wahrscheinlich.

Im Grossen und Ganzen fühlt sich Ruby ähnlich wie Python an, mit ein paar syntaktischen Änderungen, aber etwas weniger komplex. Zum Beispiel gibt die Funktion map() in Python einen Generator zurück, der in eine Liste umgewandelt werden muss, um bestimmte Elemente zu erhalten (aus Leistungsgründen), was in Ruby nicht der Fall ist. Entsprechend ist

list(map(lambda x: x * 2, list_of_numbers))

in Python

list_of_numbers.map{ |x| x * 2 }

in Ruby, was kürzer, lesbarer und verkettbar ist (wie in JavaScript). Mir gefällt auch, dass einige syntaktische Muster wie der ternäre Operator oder “for”-Schleifen eher im C-Stil gehalten sind als die pythonischen Abstraktionen. Eine Sache, die ich in Pyhton wirklich mag, ist die Kraft der print() Funktion im Vergleich zu Ruby’s puts, die Sachen viel weniger schön ausgibt.

Ruby hat mir wirklich gut gefallen, und ich werde in Betracht ziehen, es für Aufgaben zu verwenden, für die Python sonst gut geeignet wäre. Ich muss mir noch die Paket-Auswahl (Plot-Bibliothek, wissenschaftliches Zeug) ansehen, aber AoC in Ruby zu schreiben hat grossen Spass gemacht!

def get_instructions(inp, index)
  type = KEYPADS[index]
 
  comb = ""
  pos = POS[index]
 
  inp.each do |key|
    source = pos
    target = (eval type)[key]
 
    ydist = target[0] - source[0]
    xdist = target[1] - source[1]
    
    inv = (eval type)["inv"]
 
    if horfirst(source, target, inv)
      # left first
      comb += "".rjust(xdist.abs(), xdist > 0 ? ">" : "<")
      comb += "".rjust(ydist.abs(), ydist > 0 ? "v" : "^")
    else

Kotlin

Erfahrung:
Spass:

Kotlin war nicht allzu wild. Es ist eine Sprache, die sich wie TypeScript anfühlt und Java-Bibliotheken verwenden kann.

Eine interessante Eigenschaft, die ich gefunden habe, ist, dass val eine konstante Variable deklariert, var eine veränderbare Variable. Ansonsten ist es einfach nur… TS, dem ist nichts mehr hinzuzufügen.

fun main() {
  val inputStream: InputStream = File("input_smol.txt").inputStream()
  val lineList = mutableListOf<String>()
 
  inputStream.bufferedReader().forEachLine { lineList.add(it) }
 
  var sum: Long = 0
 
  // val changes = listOf(-1, -1, 0, 2)
  val changes = listOf(-2,1,-1,3)
  var max: Long = 0
  var maxStr = ""
  var iter = 0
 
  for(i in -9..9) {
    for(j in -9..9) {
      println(iter)
      iter+=1
      for(k in -9..9) {
        for(l in -9..9) {

Zig

Erfahrung:
Spass:

Ich hatte viel Gutes über Zig gehört. Und jetzt, wo ich es tatsächlich benutzt habe, denke ich, dass es die schlechteste Erfahrung von allen Sprachen auf dieser Liste war (Lisp ausgenommen). Vielleicht habe ich sie nur nicht so benutzt, wie ich es mir vorgestellt habe, aber die Dokumentation sagt einem nicht wirklich, wie man die Sprache benutzt (dazu später mehr).

Das erste, was mir aufgefallen ist, ist das Fehlen eines string oder char Typs. Jeder Text ist einfach ein Array von u8, was eine vorzeichenlose 8-Bit Ganzzahl ist. Sogar C hat einen char-Typ (der hier genauso funktioniert wie ein u8, aber der Compiler weiss zumindest, dass es sich um Text handeln soll). Das erwies sich als wirklich mühsam für diesen Tag von AoC, an dem ich eine Liste von “Netzwerkdreiecken” speichern wollte, die jeweils eine Liste von Hosts sind, die eine Liste von u8 sind. Hässlich.

Als nächstes ist Zig ein absoluter Typen-Clutserfuck, der nirgends richtig erklärt wird. Das Slicing eines Arrays von u8 ([]u8) sollte einfach ein anderes Array von u8 zurückgeben, richtig? Sorry, nein, es ist tatsächlich const [2]u8, was nicht kompatibel mit [_]u8 oder []u8 ist.

Versuchen wir nun, zwei Strings zu vergleichen. Da es in Zig keine Strings gibt, können wir den praktischen ==-Operator nicht verwenden. Wir müssen vielmehr std.mem.eql([type], [str1], [str2]) verwenden, das die Memory der beiden Objekte vergleicht. Für AoC musste ich jedoch ein einzelnes Zeichen vergleichen, aber std.mem.eql verlangt ein Array. Daher müssen wir ein Zeichen in ein []u8 umwandeln, indem wir… [str[0]] verwenden? Nein, natürlich nicht. Dies ist nicht C von 1972, dies ist Zig von 2016! Wir müssen stattdessen &[_]u8{str[0]} benutzen!.

Zig war voll von solchem Scheiss.

Das wäre ja alles noch in Ordnung, wenn die Docs das erklären würden. Tun sie aber nicht. Die Beispiele in der Dokumentation sind extrem kurz und zeigen keine komplexeren Beispiele, und nichts wird wirklich ausführlich genug erklärt.

pub fn main() !void {
    var splits = split(u8, data, "\n");
    var edges = ArrayList(u8).init(std.heap.page_allocator);
    defer edges.deinit();
 
    while (splits.next()) |line| {
        if (line.len == 0) continue;
        const host1 = line[0..2];
        const host2 = line[3..5];
        if (compStrings(host1, host2)) {
            // host1 first
            const row: []const u8 = host1 ++ host2;
            try edges.appendSlice(row);
        } else {
            // host2 first
            const row: []const u8 = host2 ++ host1;
            try edges.appendSlice(row);
        }
    }
 

Octave

Erfahrung:
Spass:

Octave ist eine Sprache, die für Matrixoperationen in der Technik und der Mathematik entwickelt wurde (MATLAB-Ersatz) und als solche nicht wirklich für Advent of Code-Rätsel geeignet ist. Es hat jedoch recht gut mitgehalten.

Das erste, was man bei Octave verstehen muss, ist, dass jedes Array (genannt cell array) eine 2D-Matrix ist, auch wenn es nur ein einzelner Wert ist. Das macht die Schleifenbildung über die richtige Dimension manchmal etwas verwirrend, aber wenn man sowohl rows als auch columns ausprobiert, ist das Problem normalerweise gelöst.

Eine weitere interessante Sache an Octave ist, dass jede Zeile, die nicht mit einem ; abgeschlossen wird, ihr Ergebnis auf stdout ausgibt. Somit ist die Zeile 5 eine gültige Anweisung und gibt 5 auf stdout aus.

Insgesamt war Octave ziemlich cool, und es bietet eine Menge praktischer Funktionen für alle möglichen Dinge. Allerdings finde ich die Dokumentation ein wenig umständlich zu navigieren.

DATA = fileread("input.txt");
DATA_SPLT = strsplit(DATA, "\n");
 
contains = @(str, pattern) ~cellfun('isempty', strfind(str, pattern));
 
INIT = DATA_SPLT(contains(DATA_SPLT, ":"));
INST = DATA_SPLT(contains(DATA_SPLT, "->"));
 
STATE = struct();
for i = 1:columns(INIT)
  FIELDS = strsplit(INIT{1, i}, ": ");
  STATE.(FIELDS{1, 1}) = str2num(FIELDS{1, 2});
endfor
 
while(columns(INST) > 0)
  DEL = [];
  for i = 1:columns(INST)
    RES = strsplit(INST{1,i}, " -> "){1, 2};
    O1 = strsplit(INST{1,i}, " "){1, 1};
    O2 = strsplit(INST{1,i}, " "){1, 3};

Lua

Erfahrung:
Spass:

Der einzige Ort, an dem ich bisher mit Lua in Berührung kam, war die Konfiguration von Neovim. Jetzt, wo ich es benutzt habe, um ein echtes Problem zu lösen, muss ich sagen, dass ich beeindruckt bin, wie einfach Lua ist - sogar einfacher als Python oder Ruby. Ich denke, es ist eine grossartige Sprache für den Einstieg in die Programmierung.

Wenn man zum Beispiel an ein bestehendes Array anhängen will, muss man in den meisten Sprachen irgendeine Art von append oder push Operation durchführen. Nicht in Lua: Schreib einfach einen Wert an einen beliebigen Index, und Lua kümmert sich um den komplizierten Speicherkram für Sie. Cool!

function lines_from(file)
  local lines = {}
  for line in io.lines(file) do
    lines[#lines + 1] = line
  end
  return lines
end
 
local file = 'input.txt'
local lines = lines_from(file)
 
local keys = {}
local locks = {}
 
for i = 1, (#lines+1) / 8 do
  local start = (i - 1) * 8 + 1
  local entry = {}
  for col = 1, 5 do
    local s = 0
    for row = 1,5 do
import numpy as np
import pandas as pd
 
data = pd.read_csv('input.txt', delimiter='\t', header=None)
 
col1 = data[0].sort_values().values
col2 = data[1].sort_values().values
 
a = np.absolute(col1 - col2)
 
print('Part 1:', sum(a))
 
### part 2
 
s = 0
 
for i in col1:
    ii = np.where(col2 == i)[0]
    s += i * len(ii)
 
print('Part 2:', s)

Comments

Subscribe via RSS.

MIT 2024 © Alexander Schoch.