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.