Os guards ajudam a aumentar a força na utilização de pattern matching em funções. Eles podem servir de controle de fluxo para uma determinada função ou até garantir que um tipo especifico esta passando.
Vamos supor que precisamos imprimir o dado que é passado como argumento da função, mas temos uma peculiaridade. Podemos passar tanto número inteiro, string e até map. A forma como esses três devem ser imprimidos é diferente. Vamos a um teste simples:
test/printer_test.exs
defmodulePrinterTestdouseExUnit.Case test "print/1"do assert Printer.print(1) =="Number: 1" assert Printer.print("Hello") =="Text: Hello" assert Printer.print(%{name: "iago"}) =="Map with name: iago"endend
Vamos rodar-lo:
mixtesttest/printer_test.exsCompiling1file (.ex)Generatedhello_worldappwarning:Printer.print/1isundefined (module Printerisnotavailableorisyettobedefined)Invalidcallfoundat3locations:test/printer_test.exs:5:PrinterTest."test print/1"/1test/printer_test.exs:6:PrinterTest."test print/1"/1test/printer_test.exs:7:PrinterTest."test print/1"/11) test print/1 (PrinterTest)test/printer_test.exs:4** (UndefinedFunctionError) functionPrinter.print/1 is undefined (modulePrinterisnotavailable)code:assertPrinter.print(1) =="Number: 1"stacktrace:Printer.print(1)test/printer_test.exs:5: (test)Finishedin0.02seconds (0.00s async,0.02ssync)1test,1failure
Recebemos relatório de erro por não possuir o módulo e função utilizados. Vamos cria-los. Para reoslver esse problema, poderiamos usar cond facilmente:
Isso parece bom. Resolve nosso problema de forma elegante, acredito. Mas estamos lidando com um problema que resolvemos basicamente uma linha por tipo. Mas e se tivermos que fazer uma operação maior. Começa a se formar uma bagunça. Vamos adicionar uma operação superficial para simular isso. Vamos alterar o primeiro teste para saber se um número é par ou impar.
test/printer_test.exs
defmodulePrinterTestdouseExUnit.Case test "print/1"do assert Printer.print(2) =="Number 2 is even" assert Printer.print(1) =="Number 1 is odd" assert Printer.print("Hello") =="Text: Hello" assert Printer.print(%{name: "iago"}) =="Map with name: iago"endend
Vamos alterar nossa implementação, utilizando ainda o cond/2.
lib/printer.ex
defmodulePrinterdorequireIntegerdefprint(arg) doconddo is_integer(arg) ->if (Integer.is_even(arg) ==true) do"Number #{arg} is even"else"Number #{arg} is odd"endis_bitstring(arg)->"Text: #{arg}"is_map(arg)->"Map with name: #{arg.name}"true->"Noops"endendend
A leitura começou a ficar um pouco bagunçada, nao? Isso foi uma mudança pequena. Você pode me dizer que poderiamos usar uma função ali. E você está certo em relação a isso. Mas ao invez disso, porque não isolamos a função print/1 para cada tipo de dado de entrar? Podemos fazer isso utilizando clauses. Elas são definidas ao lado da definição da função iniciando com a palavra chave when e uma operação a seguir. Vamos la:
lib/printer.ex
defmodulePrinterdorequireIntegerdefprint(arg) whenis_integer(arg) doif (Integer.is_even(arg) ==true) do"Number #{arg} is even"else"Number #{arg} is odd"endenddefprint(arg) whenis_bitstring(arg), do: "Text: #{arg}"defprint(arg) whenis_map(arg), do: "Map with name: #{arg.name}"end
Não preciso falar o quanto a leitura melhorou nesse exemplo certo? Temos diversas outras vantagens como, facilidade de extração e adição de novos tipos. Você pode perceber que as funções são controladas pelas funções seguidas do when. É ali que os guards moram.
Podemos pensar que guards podem ser usados até em operações complexas. Mas existe uma regras de utilização dos guards. Não podemos adicionar módulos nosso em nossas condicionais. Precisamos utilizar somente o básico da linguage. Foi uma decisão deliberada pela linguagem para evitar certos problemas e complexidades em cima dos guards.