Punto libre en Haskell
coding-style pointfree (5)
Tengo este código que quiero hacer sin puntos;
(/kt -> chr $ a + flip mod 26 (ord k + ord t -2*a))
¿Cómo puedo hacer eso?
¿También hay algunas reglas generales para el estilo sin puntos que no sean "piense en esto y encuentre algo"?
¿También hay algunas reglas generales para el estilo sin puntos que no sean "piense en esto y encuentre algo"?
Siempre puede hacer trampa y usar la herramienta "pl" de lambdabot (ya sea yendo a #haskell en freenode o usando, por ejemplo, ghci en ácido ). Para su código pl le da:
((chr . (a +) . flip mod 26) .) . flip flip (2 * a) . ((-) .) . (. ord) . (+) . ord
Lo cual no es realmente una mejora si me preguntas.
Conéctese en IRC, #haskell , y pregunte lambdabot! :
<you> @pl (/k t -> chr $ a + flip mod 26 (ord k + ord t -2*a))
<lambdabot> [the answer]
Definitivamente hay un conjunto de trucos para transformar una expresión en un estilo sin puntos. No pretendo ser un experto, pero aquí hay algunos consejos.
Primero, desea aislar los argumentos de la función en el término más a la derecha de la expresión. Sus herramientas principales aquí serán flip
y $
, usando las reglas:
f a b ==> flip f b a
f (g a) ==> f $ g a
donde f
y g
son funciones, y a
y b
son expresiones. Así que para empezar:
(/k t -> chr $ a + flip mod 26 (ord k + ord t -2*a))
-- replace parens with ($)
(/k t -> chr $ (a +) . flip mod 26 $ ord k + ord t - 2*a)
-- prefix and flip (-)
(/k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ ord k + ord t)
-- prefix (+)
(/k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ (+) (ord k) (ord t))
Ahora necesitamos salir en el lado derecho. Para hacer esto, usa la regla:
f (g a) ==> (f . g) a
Y entonces:
-- pull the t out on the rhs
(/k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ ((+) (ord k) . ord) t)
-- flip (.) (using a section)
(/k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ ((. ord) $ (+) (ord k)) t)
-- pull the k out
(/k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ ((. ord) . ((+) . ord)) k t)
Ahora, necesitamos convertir todo a la izquierda de k
y t
en un gran término de función, de modo que tengamos una expresión de la forma (/kt -> fkt)
. Aquí es donde las cosas se ponen un poco alucinantes. Para empezar, tenga en cuenta que todos los términos hasta el último $
son funciones con un solo argumento, por lo que podemos redactarlos:
(/k t -> chr . (a +) . flip mod 26 . flip (-) (2*a) $ ((. ord) . ((+) . ord)) k t)
Ahora, tenemos una función de tipo Char -> Char -> Int
que queremos componer con una función de tipo Int -> Char
, produciendo una función de tipo Char -> Char -> Char
. Podemos lograr eso usando la regla (muy extraña)
f (g a b) ==> ((f .) . g) a b
Eso nos da:
(/k t -> (((chr . (a +) . flip mod 26 . flip (-) (2*a)) .) . ((. ord) . ((+) . ord))) k t)
Ahora solo podemos aplicar una reducción beta:
((chr . (a +) . flip mod 26) .) . (flip flip (2*a) . ((-) . ) . ((. ord) . (+) .ord))
Estoy asumiendo que el objetivo de su liberación de puntos es hacer que el código sea más conciso y más legible. Por lo tanto, creo que es sensato hacer también otras refactorizaciones hacia la simplificación, lo que podría facilitar la eliminación de las variables.
(/k t -> chr $ a + flip mod 26 (ord k + ord t - 2*a))
En primer lugar, el flip
es innecesario:
(/k t -> chr $ a + (ord k + ord t - 2*a) `mod` 26)
A continuación, usaría el nombre y la conquista para factorizar una subfunción de uso independiente:
encode_characters k t = chr $ encode (ord k) (ord t)
encode x y = (x + y - 2*a) `mod` 26 + a
También le puse un nombre a la primera expresión para hacerlo más claro y reutilizable. encode_characters
ahora es fácil de hacer sin puntos utilizando la técnica de @Nefrubyr:
encode_characters = chr . encode `on` ord
En cuanto a la segunda expresión, no puedo producir un formulario que sea más legible que el que se muestra en las otras respuestas y todos son menos legibles que la forma puntual. Por lo tanto, sugeriría dejar de refactorizar en este punto y admirar la limpieza y reutilización del código resultante.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
PD: como un ejercicio, dependiendo del contexto del problema, alguna modificación leve de las interfaces de la función (qué datos en qué forma se pasa a las funciones) podría generar más simplificaciones al generalizar el problema.
A. Implementar y simplificar la función encode_n_characters :: [Char] -> Char
donde encode_characters kt = encode_n_characters [k, t]
. ¿Es el resultado más simple que la función especializada de dos argumentos?
B. Implementar una función de encode''
definida vía encode'' (x + y) = encode xy
y encode_characters
implementar encode''
utilizando esta función. ¿Alguna de las funciones se vuelve más simple? ¿Es la implementación más sencilla en general? ¿Es la encode''
más o menos reutilizable que la encode
?
Para activar una función
func x y z = (some expression in x, y and z)
En forma libre de puntos, generalmente trato de seguir lo que se hace con el último parámetro z
y escribo la función como
func x y z = (some function pipeline built using x and y) z
Entonces puedo cancelar las z
s para obtener
func x y = (some function pipeline built using x and y)
Luego, repetir el proceso para y y x debería terminar con func
en forma libre de puntos. Una transformación esencial a reconocer en este proceso es:
f z = foo $ bar z -- or f z = foo (bar z)
<=> f z = foo . bar $ z
<=> f = foo . bar
También es importante recordar que con una evaluación parcial, puede "separar" el último argumento de una función:
foo $ bar x y == foo . bar x $ y -- foo applied to ((bar x) applied to y)
Para su función particular, considere el flujo que k
y t
atraviesan:
- Aplicar el orden a cada uno de ellos.
- Añade los resultados
- Resta 2 * a
- Toma el resultado mod 26.
- Agrega un
- Aplicar
chr
Entonces, como primer intento de simplificación, obtenemos:
func k t = chr . (+a) . (`mod` 26) . subtract (2*a) $ ord k + ord t
Tenga en cuenta que puede evitar flip
usando una sección en mod
, y secciones usando -
ensuciarse en Haskell, así que hay una función de subtract
(chocan con la sintaxis para escribir números negativos: (-2)
significa negativo 2, y lo mismo que subtract 2
).
En esta función, ord k + ord t
es un excelente candidato para usar Data.Function.on
( link ). Este útil combinador nos permite reemplazar ord k + ord t
con una función aplicada a k
y t
:
func k t = chr . (+a) . (`mod` 26) . subtract (2*a) $ ((+) `on` ord) k t
Ahora estamos muy cerca de tener
func k t = (function pipeline) k t
y por lo tanto
func = (function pipeline)
Desafortunadamente, Haskell es un poco desordenado cuando se trata de componer una función binaria con una secuencia de funciones únicas, pero hay un truco (veré si puedo encontrar una buena referencia para ella), y terminamos con:
import Data.Function (on)
func = ((chr . (+a) . (`mod` 26) . subtract (2*a)) .) . ((+) `on` ord)
que es casi un buen canal de funciones sin puntos, a excepción de ese truco de composición feo. Al definir el operador .:
Sugerido en los comentarios de esta página , esto se ajusta un poco a:
import Data.Function (on)
(.:) = (.).(.)
func = (chr . (+a) . (`mod` 26) . subtract (2*a)) .: ((+) `on` ord)
Para pulir esto un poco más, puede agregar algunas funciones de ayuda para separar la conversión de la letra <-> Int de la aritmética de cifrado César . Por ejemplo: letterToInt = subtract a . ord
letterToInt = subtract a . ord