Funções como dados
Como muitas outras linguagens de programação modernas – incluindo todas as linguagens funcionais (ML, Haskell, Scheme, Scala, Clojure etc.) – o Kind trata as funções como cidadãos de primeira classe, permitindo que sejam passadas como argumentos para outras funções, retornados como resultados, armazenados em estruturas de dados, etc.
Funções de Alta Ordem (Higher-Order Functions.)
Funções que manipulam outras funções são frequentemente chamadas de funções de alta ordem (ou ainda de "ordem superior"). Aqui está um exemplo simples:
Doit3times <x> (f: x -> x) (n: x) : x
Doit3times f x = (f (f (f x)))
Test_doit3times1 : Equal (Doit3times (x => MinusTwo x) 9n) 3n
Test_doit3times1 = Equal.refl
Test_doit3times2 : Equal (Doit3times (x => Notb x) Bool.true) (Bool.false)
Test_doit3times2 = Equal.refl
Filtro
Aqui está uma função de alta ordem mais útil, pegando uma lista de xs e um predicado em x (uma função de x para Bool) e “filtrando” a lista, retornando uma nova lista contendo apenas aqueles elementos para os quais o predicado retorna True.
Filter <x> (test: x -> Bool) (xs: List x) : List x
Filter test List.nil = []
Filter test (List.cons head tail) =
Bool.if (test head) (List.cons head (Filter test tail)) (Filter test tail)
Por exemplo, se aplicarmos o filtro de "é par" numa lista de números, ela nos retornará uma outra lista apena com os números pares
Test_filter1 : Equal (Filter (x => Evenb x) [1,2,3,4,5]) [2,4]
Test_filter1 = Equal.refl
Length_is_one <x> (xs: List x) : Bool
Length_is_one xs = Eql (Length xs) 1n
Test_filter2 : Equal (Filter (x => Length_is_one x) ([[1],[1,2],[3],[1,2,3],[21]])) ([[1],[3],[21]])
Test_filter2 = Equal.refl
Podemos usar filter para fornecer uma versão concisa da função Countoddmembers do capítulo Listas.
CountOddMembers (xs: List Nat) : Nat
CountOddMembers xs = Length (Filter (x => Oddb x) xs)
Test_CountOddMembers1 : Equal (CountOddMembers [1n,0n,3n,1n,4n,5n]) 4n
Test_CountOddMembers1 = Equal.refl
Test_CountOddMembers2 : Equal (CountOddMembers [0n 2n,4n]) 0n
Test_CountOddMembers2 = Equal.refl
Test_CountOddMembers3 : Equal (CountOddMembers []) 0n
Test_CountOddMembers3 = Equal.refl
Funções anônimas
É indiscutivelmente um pouco triste, no exemplo acima, ser forçado a definir a função Length_is_one e dar-lhe um nome apenas para poder passá-la como um argumento para filtrar, já que provavelmente nunca a usaremos novamente. Além disso, este não é um exemplo isolado: ao usar funções de ordem superior, muitas vezes queremos passar como argumentos funções “únicas” que nunca mais usaremos; ter que dar um nome a cada uma dessas funções seria tedioso. Felizmente, existe uma maneira melhor. Podemos construir uma função “on the fly” sem declará-la no nível superior ou dar-lhe um nome.
Test_anon_fun : Equal (Doit3times (x => (Mult x x)) 2n) 256n
Test_anon_fun = Equal.refl
A expressão x => (Mult x x)
pode ser lida como a função recebe um número n
e retorna n * n
Aqui está o exemplo de Filter reescrita pra usar uma função anonima:
Test_filter2 : Equal (Filter (x => (Length_is_one x)) [[1],[1,2],[2],[1,2,3],[21]]) [[1],[2],[21]]
Test_filter2 = Equal.refl
Filter_even_gt7
Use Filter com funções anônimas (em vez de definição de função) para escrever uma função Filter_even_gt7
que recebe uma lista de números naturais como entrada e retorna uma lista apenas daqueles que são pares e maiores que 7.
Filter_even_gt7 (xs: List Nat) : List Nat
Filter_even_gt7 xs = ?
Test_filter_even_gt7a: Equal (Filter_even_gt7 [1n,2n,3n,4n,5n,7n,8n,9n,10n,11n,12n]) [8n,10n,12n]
Test_filter_even_gt7a = ?
Test_filter_even_gt7b : Equal (Filter_even_gt7 [5n, 2n, 6n, 19n, 129n]) []
Test_filter_even_gt7b = ?
Uma pequena observação, o leitor mais atento percebeu que usamos uma nova notação, os n após os números, esse é um sugar syntax que o Kind possui, nós podemos escrever números naturais apenas acrescentando um n ao número, entretanto essa é uma sintaxe que pode acabar pesando para Kind. Imagine que o usuário pretende apenas adicionar o número 1 ao 1000000, é um cálculo simples e que o Kind faz com uma mão nas costas, mas fica um pouco mais pesado quando usado o sugar syntax dos números naturais, a soma será um Plus 1n 1000000n
, mas o Kind precisará verificar cada Nat.succ até o um milhão e um, ou seja, serão um milhão e um "Nat.succ" computados de forma desnecessárias. Essa sintaxe é bem útil, mas devemos usar com cuidado, o ideal é que para números grandes seja usado um U60.to_nat, que é bem mais leve para o Kind.
Partition
Use Filter para escrever uma função Partition em Kind
Partition <x> (test: x -> Bool) (xs: List x) : Pair (List x) (List x)
Partition test xs = ?
Dado um conjunto x, uma função de teste do tipo x -> Bool e uma Lista x, a função Partition deve retornar um par de listas. O primeiro membro do par é a sub-lista da lista original contendo os elementos que satisfazem o teste, e o segundo é a sub-lista contendo aqueles que falham no teste. A ordem dos elementos nas duas sub-listas deve ser a mesma da lista original.
Test_partition1 : Equal (Partition (x => Oddb x) [1n,2n,3n,4n,5n]) (Pair.new [1n,3n,5n] [2n,4n])
Test_partition1 = ?
Test_partition2 : Equal (Partition (x => Bool.false) [5n, 9n, 0n]) (Pair.new [] [5n, 9n, 0n])
Map
Outra função de alta ordem muito útil é a Map
Map <x> <y> (f: x -> y) (xs: List x) : List y
Map f List.nil = List.nil
Map f (List.cons head tail) = List.cons (f head) (Map f tail)
Ela recebe uma função f
e uma lista xs = [n1, n2, n3, ...]
e retorna a lista [f n1, f n2, f n3, ...]
, onde f
é aplicado a cada elemento de xs
. Por exemplo:
Test_map1 : Equal (Map (x => Plus 3n x) [2n, 0n, 2n]) [5n, 3n, 5n]
Test_map1 = Equal.refl
Os tipos de elementos da lista de entrada e saída não precisam ser os mesmos, pois Map aceita dois argumentos de tipo, x
e y
; dessa forma pode ser aplicada uma de números para booleanos para produzir uma lista de booleanos:
Test_map2 : Equal (Map (x => Nat.is_odd x) [2n, 1n, 2n, 5n]) [Bool.false, Bool.true, Bool.false, Bool.true]
Test_map2 = Equal.refl
Pode até ser aplicada a uma lista de números uma função que retorne uma lista de lista de booleanos:
Test_map3 = Equal (Map (x => [(Nat.is_even x), (Nat.is_odd x)]) [2n, 1n, 2n, 5n]) [[Bool.true, Bool.false], [Bool.false, Bool.true], [Bool.true, Bool.false], [Bool.false, Bool.true]]
Test_map3 = Equal.refl
Map_rev
Vamos dificultar um pouco mais as coisas. Mostre a comutatividade de Rev e Map, você pode precisar de uma função auxiliar:
Map_rev <x> <y> (f: x -> y) (xs: List x) : Equal (Map f (Rev xs)) (Rev (Map f xs))
Map_rev f xs = ?
Flat_equal
A função Map mapeia uma List x
para uma List y
usando uma função do tipo x -> y
. Podemos definir uma função semelhante, Flat_map
, que mapeia uma Lista x para uma Lista y usando uma função f do tipo x -> Lista y
. Sua definição deve funcionar "achatando" os resultados de f, assim:
Flat_equal : Equal (Flat_map ( x => ([x , (Plus x 1n), (Plus x 2n)])) [1n, 5n, 10n]) [1n, 2n, 3n, 5n, 6n, 7n, 10n, 11n, 12n]
Flat_equal = Equal.refl
Flat_map <x> <y> (f: x -> List y) (xs: List x) : List y
Flat_map f xs = ?
Test_flat_map1 : Equal (Flat_map (x => [x, x, x]) [1n, 5n, 4n]) [1n, 1n, 1n, 5n, 5n, 5n, 4n, 4n, 4n]
Test_flat_map1 = ?
As listas não são o único tipo indutivo para o qual podemos escrever uma função Map. Aqui está a definição de mapa para o tipo Maybe:
Maybe_map <x> <y> (f: x -> y) (a: Maybe x) : Maybe y
Maybe_map f Maybe.none = Maybe.none
Maybe_map f (Maybe.some x) = Maybe.some (f x)
Fold
Uma função de ordem superior ainda mais poderosa é chamada Fold. Esta função é a inspiração para a operação “reduce” que está no coração da estrutura de programação distribuída map/reduce do Google.
Fold <x> <y> (f: x -> y -> y) (xs: List x) (a: y) : y
Fold f List.nil a = a
Fold f (List.cons head tail) a = f head (Fold f tail a)
Test_fold1 : Equal (Fold (x => y => (Bool.and x y)) [Bool.true, Bool.true, Bool.false] Bool.false) Bool.false
Test_fold1 = ?
Test_fold2 : Equal (Fold (x => y => (* x y)) [1, 2, 3, 4] 1) 24
Test_fold2 = ?
Test_fold3 : Equal (Fold (x => y => (Concat x y)) [[1], [], [2, 3], [], [4]] [5, 6, 7]) [1, 2, 3, 4, 5, 6, 7]
Test_fold3 = ?
Fold_types_different
Observe que o tipo Fold é parametrizado por duas variáveis de tipo, x e y, e o parâmetro f é um operador binário que recebe um x e um y e retorna um y. Você consegue pensar em uma situação em que seria útil que x e y fossem diferentes?
Funções que constroem funções
A maioria das funções de ordem superior sobre as quais falamos até agora usam funções como argumentos. Vejamos alguns exemplos que envolvem o retorno de funções como resultados de outras funções. Para começar, aqui está uma função que recebe um valor x (extraído de algum tipo x) e retorna uma função de Nat para x que retorna x sempre que é chamada, ignorando seu argumento Nat
Constfun <y> (x: y) : Nat -> y
Constfun x = y => x
Ftrue : Nat -> Bool
Ftrue = Constfun Bool.true
Constfun_example1 : Equal ((Ftrue) 0n) Bool.true
Constfun_example1 = Equal.refl
Constfun_example2 : Equal ((Constfun 5n) 99n) 5n
Constfun_example2 = Equal.refl
Plus3 : Nat -> Nat
Plus3 = n => Plus 3n n
Test_plus3_1 : Equal ((Plus3) 4n) 7n
Test_plus3_1 = Equal.refl
Test_plus3_2 : Equal (Doit3times (Plus3) 0n) 9n
Test_plus3_2 = Equal.refl
Test_plus3_3 : Equal (Doit3times (x => Plus 3n x) 0n) 9n
Test_plus3_3 = Equal.refl