Resolva o problema de uma instituição de caridade com a linguagem de programação Julia
Veja como Julia difere de Java, Python e Groovy para resolver o problema do mundo real de um banco de alimentos.
Tenho escrito uma série de artigos sobre como resolver um problema interessante, pequeno e um tanto incomum em diferentes linguagens de programação (Groovy, Python e Java até agora).
Resumidamente, o problema é como desempacotar os fornecimentos a granel nas suas unidades (por exemplo, dividir um pacote de 10 sacos de meio quilo do seu café favorito) e reembalá-los em cestos de valor semelhante para distribuir aos vizinhos em dificuldades na comunidade.
As três soluções que já explorei construíram listas do número de embalagens a granel adquiridas. Consegui isso usando mapas em Groovy, dicionários em Python e tuplas implementadas como classes utilitárias em Java. Usei a funcionalidade de processamento de lista em cada idioma para descompactar os pacotes em massa em uma lista de seus constituintes, que modelei usando mapas, dicionários e tuplas, respectivamente. Eu precisava adotar uma abordagem iterativa para mover unidades de uma lista para cestos; essa abordagem iterativa era bastante semelhante de uma linguagem para outra, com a pequena diferença de que eu poderia usar loops for {...}
em Groovy e Java e precisava de while…:
em Python. Mas, no geral, eles usaram soluções muito semelhantes, com dicas de programação funcional e comportamento encapsulado em objetos aqui e ali.
Conheça Júlia
Neste artigo, explorarei o mesmo problema em Julia, o que (entre outras coisas) significa deixar de lado os paradigmas de programação funcional e orientada a objetos aos quais estou acostumado. Tenho dificuldade com linguagens que não são orientadas a objetos. Tenho programado em Java desde 1997 e em Groovy desde 2008, então estou acostumado a ter dados e comportamento agrupados. Além de gostar da aparência do código em que as chamadas de método ficam suspensas em objetos ou, às vezes, em classes, eu realmente gosto da maneira como a documentação da classe empacota quais dados são tratados pela classe e como eles são tratados. Isso me parece tão "natural" agora que aprender uma linguagem cuja documentação descreve tipos e funções separadamente parece difícil para mim.
E por falar em aprender um idioma, sou um verdadeiro neófito quando se trata de Julia. Gosto de sua orientação para os tipos de problemas que normalmente preciso resolver (por exemplo, dados, cálculos, resultados). Gosto do desejo de velocidade. Gosto da decisão de fazer de Julia uma linguagem na qual problemas complicados possam ser resolvidos usando uma abordagem modular e iterativa. Gosto da ideia de disponibilizar ótimas bibliotecas analíticas existentes. Mas meu júri ainda não decidiu sobre o design não orientado a objetos. Também pareço usar abordagens funcionais em minha programação Groovy e Java com mais frequência, então acho que posso sentir falta disso em Julia.
Mas chega de especulação, vamos codificar alguma coisa!
A solução Júlia
Minha primeira decisão é como implementar o modelo de dados. Julia suporta tipos compostos, aparentemente semelhantes a struct
em C, e Julia ainda usa a palavra-chave struct
. É importante ressaltar que uma struct
é imutável (a menos que seja declarada uma estrutura mutável
), o que é bom para esse problema, pois os dados não precisam sofrer mutação.
Seguindo a abordagem que adotei na solução Java, o Unit struct
pode ser definido como:
struct Unit
item::String
brand::String
price::Int
end
Da mesma forma, Pack
é definido como o pacote em massa de instâncias de Unit
:
struct Pack
unit::Unit
count::Int
Pack(item, brand, unitCount,p ackPrice) =
new(Unit(item, brand, div(packPrice,unitCount)), unitCount)
end
Há uma coisa interessante aqui: um “construtor interno” de Julia. Na solução Java, decidi que as unidades dentro dos pacotes a granel são (na minha opinião, pelo menos) uma parte do pacote a granel e não algo visto externamente, então decidi que queria passar o item, marca, número de unidades, e preço do pacote e faça com que o objeto Pack
crie sua unidade internamente. Farei a mesma coisa aqui.
Como Julia não é orientada a objetos, não posso adicionar métodos a Pack
para fornecer preço unitário versus preço do pacote ou descompactá-lo em uma lista de instâncias de Unit
. Posso declarar funções "getter" que realizam as mesmas tarefas. (Provavelmente não preciso disso, mas farei isso mesmo assim para ver como funcionam os métodos 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))
O método unpack()
é bastante semelhante ao método de mesmo nome que declarei na classe Java Pack
. A função Iterators.repeated(thing,N)
cria um iterador que entregará N
cópias de thing
. A função Iterators.collect
(iterator
) processa o iterador
para produzir um array composto pelos elementos que ele entrega.
Finalmente, a estrutura comprada
:
struct Bought
pack::Pack
count::Int
end
unpack(bought::Bought) =
Iterators.collect(Iterators.flatten(Iterators.repeated(unpack(bought.pack),
bought.count)))
Mais uma vez, estou criando um array de um array de instâncias Pack
descompactadas (ou seja, unidades) e usando Iterators.flatten()
para transformar isso em um array simples.
Agora posso construir a lista do que comprei:
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)]
Estou começando a ver um padrão aqui... surpreendentemente se parece com a solução Java para esse problema. Como então, isso mostra que comprei um pacote de Arroz Melhor Família contendo 10 unidades que custaram 5650 (usando aquelas unidades monetárias malucas, como nos outros exemplos). Comprei um pacote a granel de 10 sacos de arroz e comprei 10 pacotes a granel de um saco de espaguete cada.
Com os pacotes de listas do que comprei, agora posso desempacotar nas unidades antes de trabalhar na redistribuição:
units = Iterators.collect(Iterators.flatten(unpack.(packs)))
O que está acontecendo aqui? Bem, uma construção como unpack.(packs)
- isto é, o ponto entre o nome da função e a lista de argumentos - aplica a função unpack()
a cada elemento no list pacotes
. Isto irá gerar uma lista de listas correspondentes aos grupos descompactados de Packs
que comprei. Para transformar isso em uma lista simples de unidades, aplico Iterators.flatten()
. Como Iterators.flatten()
é preguiçoso, para que o nivelamento aconteça, eu o envolvo em Iterators.collect()
. Esse tipo de composição de funções segue o espírito da programação funcional, mesmo que você não veja as funções encadeadas, como os programadores que escrevem funcionalmente em JavaScript, Java ou o que você conhece.
Uma observação é que a lista de unidades criada aqui é na verdade um array cujo índice inicial é 1, não 0.
Com as unidades sendo a lista de unidades compradas e desembaladas, agora posso reembalá-las em cestos.
Aqui está o código, que não é excepcionalmente diferente das versões em 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
Alguns esclarecimentos, por números de linha:
- Linhas 1–3: Configure os valores ideal e máximo a serem carregados em qualquer cesto e inicialize o gerador de números aleatórios do Groovy e o número do cesto
- Linhas 4–29: Este loop
while
redistribui unidades em cestos, desde que haja mais unidades disponíveis - Linhas 5–7: Incremente o número (global) do cesto, obtenha um novo cesto vazio (uma matriz de instâncias de
Unit
) e defina seu valor como 0 - Linha 8 e 9–23: Contanto que eu possa adicionar unidades ao cesto…
- Linha 10: Obtém um número aleatório entre zero e o número de unidades restantes menos 1
- Linha 11: Pressupõe que não consigo encontrar mais unidades para adicionar
- Linhas 12–22: Este loop
for
, começando no índice escolhido aleatoriamente, tentará encontrar uma unidade que possa ser adicionada ao cesto - Linhas 13–14: Descubra qual unidade observar (lembre-se de que os arrays começam no índice 1) e obtenha-o
- Linhas 15–21: Posso adicionar esta unidade ao cesto se restarem apenas algumas ou se o valor do cesto não for muito alto depois que a unidade for adicionada e se essa unidade ainda não estiver no cesto
- Linhas 16–18: Adicione a unidade à cesta, aumente o valor da cesta pelo preço unitário e remova a unidade da lista de unidades disponíveis
- Linhas 19–20: Enquanto sobrarem unidades, posso adicionar mais, então saia desse ciclo para continuar procurando
- Linha 22: Ao sair deste loop
for
, se eu tiver inspecionado todas as unidades restantes e não conseguir encontrar nenhuma para adicionar ao cesto, o cesto estará completo; caso contrário, encontrei um e posso continuar procurando por mais - Linha 23: Ao sair deste loop
while
, o cesto está tão cheio quanto consigo, então… - Linhas 24–28: Imprima o conteúdo do cesto e as informações das unidades restantes
- Linha 29: Quando saio deste loop, não restam mais unidades
A saída da execução deste código é bastante semelhante à saída de outros programas:
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
O último cesto é abreviado em conteúdo e valor.
Pensamentos finais
Mais uma vez, a manipulação da lista orientada por números aleatórios parece tornar a parte do “código funcional” do programa bastante semelhante às versões Groovy, Python e Java. Para minha alegria, encontrei um bom suporte de programação funcional em Julia, pelo menos no que diz respeito ao processamento direto de lista necessário para este pequeno problema.
Dado que o esforço principal gira em torno dos loops for
e while
, em Julia, não vejo nenhuma construção semelhante a:
for (boolean canAdd = true; canAdd; ) { … }
Isso significa que tenho que declarar a variável canAdd
fora do loop while
. O que é uma pena – mas não é uma coisa terrível.
Sinto falta de não poder anexar comportamento diretamente aos meus dados, mas isso é apenas o meu apreço pela exibição da programação orientada a objetos. Certamente não é um grande impedimento neste programa; no entanto, a correspondência com um autor gentil sobre minha versão Java me fez perceber que eu deveria ter construído uma classe para encapsular completamente a função de distribuição em algo como uma lista de obstáculos, que o programa principal simplesmente imprimiria. Esta abordagem não seria viável em uma linguagem não orientada a objetos como Julia.
Coisas boas: baixa cerimônia, confere; manipulação de lista decente, verifique; código compacto e legível, verifique. Em suma, uma experiência agradável, apoiando a ideia de que Julia pode ser uma escolha decente para resolver “problemas comuns” e como linguagem de script.
Da próxima vez, farei este exercício em Go.