Ricerca nel sito web

Risolvi il problema di un ente di beneficenza con il linguaggio di programmazione Julia


Scopri come Julia differisce da Java, Python e Groovy per risolvere il problema reale di una banca alimentare.

Ho scritto una serie di articoli sulla risoluzione di un problema carino, piccolo e alquanto insolito in diversi linguaggi di programmazione (Groovy, Python e Java finora).

In breve, il problema è come disimballare le scorte sfuse nelle loro unità (ad esempio, dividendo una confezione da 10 di sacchi da mezzo chilo del tuo caffè preferito) e riconfezionarle in cesti di valore simile da distribuire ai vicini in difficoltà nella comunità.

Le tre soluzioni che ho già esplorato contengono elenchi del numero di pacchi sfusi acquistati. Ho ottenuto questo risultato utilizzando mappe in Groovy, dizionari in Python e tuple implementate come classi di utilità in Java. Ho utilizzato la funzionalità di elaborazione degli elenchi in ciascuna lingua per decomprimere i pacchetti sfusi in un elenco dei loro costituenti, che ho modellato utilizzando rispettivamente mappe, dizionari e tuple. Avevo bisogno di adottare un approccio iterativo per spostare le unità da un elenco ai cesti; questo approccio iterativo era abbastanza simile da un linguaggio all'altro, con la piccola differenza che potevo usare i cicli for {...} in Groovy e Java e avevo bisogno di when…: in Python. Ma tutto sommato, hanno usato soluzioni molto simili con accenni di programmazione funzionale e comportamento incapsulati qua e là negli oggetti.

Incontra Giulia

In questo articolo esplorerò lo stesso problema in Julia, il che (tra le altre cose) significa lasciare da parte i paradigmi di programmazione orientata agli oggetti e funzionale a cui sono abituato. Faccio fatica con linguaggi che non sono orientati agli oggetti. Programma in Java dal 1997 circa e in Groovy dal 2008 circa, quindi sono abituato ad avere dati e comportamenti raggruppati insieme. A parte il fatto che generalmente mi piace l'aspetto del codice in cui le chiamate ai metodi si bloccano sugli oggetti o talvolta sulle classi, mi piace molto il modo in cui la documentazione della classe raggruppa quali dati vengono gestiti dalla classe e come vengono gestiti. Questo mi sembra così "naturale" ora che imparare una lingua la cui documentazione descrive tipi e funzioni separatamente mi sembra difficile.

E a proposito di imparare una lingua, per quanto riguarda Julia sono un vero neofita. Mi piace il suo orientamento verso il tipo di problemi che in genere devo risolvere (ad esempio dati, calcoli, risultati). Mi piace il desiderio di velocità. Mi piace la decisione di rendere Julia un linguaggio in cui problemi complessi possono essere risolti utilizzando un approccio modulare e iterativo. Mi piace l'idea di rendere disponibili le grandi librerie analitiche esistenti. Ma la mia giuria è ancora fuori sul design non orientato agli oggetti. Mi sembra anche di utilizzare più spesso approcci funzionali nella mia programmazione Groovy e Java, quindi penso che potrei perderlo in Julia.

Ma basta speculazioni, codifichiamo qualcosa!

La soluzione di Giulia

La mia prima decisione è come implementare il modello di dati. Julia supporta i tipi compositi, apparentemente simili a struct in C, e Julia usa anche la parola chiave struct. Da notare che una struct è immutabile (a meno che non venga dichiarata una mutable struct), il che va bene per questo problema poiché i dati non necessitano di essere modificati.

Seguendo l'approccio che ho adottato nella soluzione Java, la Unit struct può essere definita come: 

struct Unit
       item::String
       brand::String
       price::Int
end

Allo stesso modo, Pack è definito come il pacchetto di massa di istanze Unit:

struct Pack
      unit::Unit
      count::Int
      Pack(item, brand, unitCount,p ackPrice) =
            new(Unit(item, brand, div(packPrice,unitCount)), unitCount)
end

C'è una cosa interessante qui: un "costruttore interiore" di Julia. Nella soluzione Java, ho deciso che le unità all'interno dei pacchetti di massa sono (nella mia mente, almeno) una parte del pacchetto di massa e non qualcosa visto esternamente, quindi ho deciso di voler inserire l'articolo, la marca, il numero di unità, e il prezzo del pacchetto e fare in modo che l'oggetto Pack crei la sua unità internamente. Farò la stessa cosa qui.

Poiché Julia non è orientata agli oggetti, non posso aggiungere metodi a Pack per fornire il prezzo unitario rispetto al prezzo del pacchetto o per decomprimerlo in un elenco di istanze di Unit. Posso dichiarare funzioni "getter" che svolgono gli stessi compiti. (Probabilmente non ne ho bisogno, ma lo farò comunque per vedere come funzionano i metodi Julia):

item(pack::Pack) = pack.unit.item
brand(pack::Pack) = pack.unit.brand
unitPrice(pack::Pack) = pack.unit.price
unitCount(pack::Pack) = pack.count
packPrice(pack::Pack) = pack.unit.price * pack.count
unpack(pack::Pack) = Iterators.collect(Iterators.repeated(pack.unit,pack.count))

Il metodo unpack() è abbastanza simile al metodo con lo stesso nome che ho dichiarato nella classe Java Pack. La funzione Iterators.repeated(thing,N) crea un iteratore che consegnerà N copie di thing. La funzione Iterators.collect (iterator) elabora l'iterator per produrre un array composto dagli elementi che fornisce.

Infine, la struttura Comprata:

struct Bought
      pack::Pack
      count::Int
end
unpack(bought::Bought) = 	 
     Iterators.collect(Iterators.flatten(Iterators.repeated(unpack(bought.pack),
	 bought.count)))

Ancora una volta, sto creando un array di un array di istanze Pack decompresse (cioè unità) e utilizzo Iterators.flatten() per trasformarlo in un array semplice.

Ora posso costruire l'elenco di ciò che ho acquistato:

packs = [
	Bought(Pack("Rice","Best Family",10,5650),1),
	Bought(Pack("Spaghetti","Best Family",1,327),10),
	Bought(Pack("Sardines","Fresh Caught",3,2727),3),
	Bought(Pack("Chickpeas","Southern Style",2,2600),5),
	Bought(Pack("Lentils","Southern Style",2,2378),5),
	Bought(Pack("Vegetable oil","Crafco",12,10020),1),
	Bought(Pack("UHT milk","Atlantic",6,4560),2),
	Bought(Pack("Flour","Neighbor Mills",10,5200),1),
	Bought(Pack("Tomato sauce","Best Family",1,190),10),
	Bought(Pack("Sugar","Good Price",1,565),10),
	Bought(Pack("Tea","Superior",5,2720),2),
	Bought(Pack("Coffee","Colombia Select",2,4180),5),
	Bought(Pack("Tofu","Gourmet Choice",1,1580),10),
	Bought(Pack("Bleach","Blanchite",5,3550),2),
	Bought(Pack("Soap","Sunny Day",6,1794),2)]

Sto iniziando a vedere uno schema qui... questo assomiglia sorprendentemente alla soluzione Java a questo problema. Come allora, questo dimostra che ho comprato un pacchetto di Best Family Rice contenente 10 unità che costano 5650 (usando quelle unità monetarie pazze, come negli altri esempi). Ho comprato una confezione sfusa da 10 sacchetti di riso e ho comprato 10 confezioni sfuse da un sacchetto ciascuna di spaghetti.

Con l'elenco dei pacchetti di ciò che ho acquistato, ora posso disimballarli nelle unità prima di lavorare sulla loro ridistribuzione:

units = Iterators.collect(Iterators.flatten(unpack.(packs)))

Cosa sta succedendo qui? Bene, un costrutto come unpack.(packs)—ovvero, il punto tra il nome della funzione e l'elenco degli argomenti—applica la funzione unpack() a ciascun elemento nel elenca pacchetti. Questo genererà un elenco di elenchi corrispondenti ai gruppi decompressi di Pack che ho acquistato. Per trasformarlo in un elenco semplice di unità, applico Iterators.flatten(). Poiché Iterators.flatten() è pigro, per far sì che l'appiattimento avvenga, lo avvolgo in Iterators.collect(). Questo tipo di composizione di funzioni aderisce allo spirito della programmazione funzionale, anche se non vedi le funzioni concatenate insieme, come i programmatori che scrivono funzionalmente in JavaScript, Java o qualunque cosa con cui hai familiarità.

Un'osservazione è che l'elenco di unità creato qui è in realtà un array il cui indice iniziale è 1, non 0.

Dato che le unità sono l'elenco delle unità acquistate e disimballate, ora posso occuparmi di reimballarle nei cesti.

Ecco il codice, che non è eccezionalmente diverso dalle versioni in Groovy, Python e Java:

 1	valueIdeal = 5000
 2	valueMax = round(valueIdeal * 1.1)
 3	hamperNumber = 0
       
 4	while length(units) > 0
 5	    global hamperNumber += 1
 6	    hamper = Unit[]
 7	    value = 0
 8	    canAdd = true
 9	    while canAdd
10	        u = rand(0:(length(units)-1))
11	        canAdd = false
12	        for o = 0:(length(units)-1)
13	            uo = (u + o) % length(units) + 1
14	            unit = units[uo]
15	            if length(units) < 3 || findfirst(u -> u == unit,hamper) === nothing && (value + unit.price) < valueMax
16	                push!(hamper,unit)
17	                value += unit.price
18	                deleteat!(units,uo)
19	                canAdd = length(units) > 0
20	                break
21	            end
22	        end
23	    end
24	    Printf.@printf("\nHamper %d value %d:\n",hamperNumber,value)
25	    for unit in hamper
26	        Printf.@printf("%-25s%-25s%7d\n",unit.item,unit.brand,unit.price)
27	    end
28	    Printf.@printf("Remaining units %d\n",length(units))
29	end

Alcuni chiarimenti, per numero di riga:

  • Righe 1–3: imposta i valori ideali e massimi da caricare in un determinato cesto e inizializza il generatore di numeri casuali di Groovy e il numero del cesto
  • Righe 4–29: questo ciclo while ridistribuisce le unità in cesti, purché ce ne siano di più disponibili
  • Righe 5–7: incrementa il numero del cesto (globale), ottiene un nuovo cesto vuoto (un array di istanze di Unit) e imposta il suo valore su 0
  • Riga 8 e 9–23: Finché posso aggiungere unità al cesto...
  • Riga 10: ottiene un numero casuale compreso tra zero e il numero di unità rimanenti meno 1
  • Riga 11: presuppone che non riesca a trovare più unità da aggiungere
  • Righe 12–22: questo ciclo for, iniziando dall'indice scelto casualmente, proverà a trovare un'unità che può essere aggiunta al cesto
  • Righe 13–14: scopri quale unità guardare (ricorda che gli array iniziano dall'indice 1) e ottienilo
  • Righe 15–21: posso aggiungere questa unità al cesto se ne rimangono solo poche o se il valore del cesto non è troppo alto una volta aggiunta l'unità e se quell'unità non è già nel cesto
  • Righe 16–18: aggiungi l'unità al cesto, incrementa il valore del cesto del prezzo unitario e rimuovi l'unità dall'elenco delle unità disponibili
  • Righe 19–20: finché rimangono unità, posso aggiungerne altre, quindi esci da questo ciclo per continuare a cercare
  • Riga 22: All'uscita da questo ciclo for, se ho ispezionato ogni unità rimanente e non sono riuscito a trovarne una da aggiungere al cesto, il cesto è completo; altrimenti ne ho trovato uno e posso continuare a cercarne altri
  • Riga 23: All'uscita da questo ciclo while, il cesto è pieno quanto riesco a farlo, quindi...
  • Righe 24–28: stampa il contenuto del cesto e le informazioni sulle unità rimanenti
  • Riga 29: Quando esco da questo ciclo, non rimangono più unità

L'output dell'esecuzione di questo codice sembra abbastanza simile all'output degli altri programmi:

Hamper 1 value 5020:
Tea                      Superior                     544
Sugar                    Good Price                   565
Soap                     Sunny Day                    299
Chickpeas                Southern Style              1300
Flour                    Neighbor Mills               520
Rice                     Best Family                  565
Spaghetti                Best Family                  327
Bleach                   Blanchite                    710
Tomato sauce             Best Family                  190
Remaining units 146

Hamper 2 value 5314:
Flour                    Neighbor Mills               520
Sugar                    Good Price                   565
Vegetable oil            Crafco                       835
Coffee                   Colombia Select             2090
UHT milk                 Atlantic                     760
Tea                      Superior                     544
Remaining units 140

Hamper 3 value 5298:
Tomato sauce             Best Family                  190
Tofu                     Gourmet Choice              1580
Sugar                    Good Price                   565
Bleach                   Blanchite                    710
Tea                      Superior                     544
Lentils                  Southern Style              1189
Flour                    Neighbor Mills               520
Remaining units 133

…

Hamper 23 value 4624:
Chickpeas                Southern Style              1300
Vegetable oil            Crafco                       835
Tofu                     Gourmet Choice              1580
Sardines                 Fresh Caught                 909
Remaining units 4

Hamper 24 value 5015:
Tofu                     Gourmet Choice              1580
Chickpeas                Southern Style              1300
Chickpeas                Southern Style              1300
Vegetable oil            Crafco                       835
Remaining units 0

L'ultimo cesto è abbreviato nel contenuto e nel valore.

Pensieri conclusivi

Ancora una volta, la manipolazione dell'elenco basata su numeri casuali sembra rendere la parte del "codice funzionante" del programma abbastanza simile alle versioni Groovy, Python e Java. Con mia grande gioia, ho trovato in Julia un buon supporto alla programmazione funzionale, almeno per quanto riguarda la semplice elaborazione delle liste richiesta per questo piccolo problema.

Dato che lo sforzo principale ruota attorno ai cicli for e while, in Julia, non vedo alcun costrutto simile a:

for (boolean canAdd = true; canAdd; ) { … }

Ciò significa che devo dichiarare la variabile canAdd all'esterno del ciclo while. Il che è un peccato, ma non è una cosa terribile.

Mi manca non poter associare il comportamento direttamente ai miei dati, ma questo è solo il mio apprezzamento per la programmazione orientata agli oggetti che traspare. Non è certamente un grosso ostacolo in questo programma; tuttavia, la corrispondenza con un gentile autore sulla mia versione Java mi ha fatto capire che avrei dovuto creare una classe per incapsulare completamente la funzione di distribuzione in qualcosa di simile a un elenco di cesti, che il programma principale avrebbe semplicemente stampato. Questo approccio non sarebbe fattibile in un linguaggio non orientato agli oggetti come Julia.

Cose positive: cerimonia bassa, controllo; gestione decente delle liste, controlla; codice compatto e leggibile, controlla. Tutto sommato, un'esperienza piacevole, che supporta l'idea che Julia possa essere una scelta decente per risolvere "problemi ordinari" e come linguaggio di scripting.

La prossima volta farò questo esercizio in Go.

Articoli correlati: