5. Extensi´ on del Fold
5.3. Definici´ on del Fold utilizando Functores
Ser´ıa bueno poder declarar un fold gen´erico que sirva para definir las funciones originales y las funciones extendidas, es decir, declarar un fold cuya definici´on no cambie al extender el tipo de datos sino que permanezca est´atica. Para generalizar el
fold primero debemos abstraer, para ello vamos a seguir un enfoque basado en [16]. Vamos a redefinir el tipoTerm. Primero vamos a generalizarlo, parametriz´andolo con un tipo. Luego definimos unpunto fijo para los constructores:
abstract classTerm[A]
case classNum[A](value: Int)extendsTerm[A]
case classPlus[A](left: A, right:A)extendsTerm[A]
case classFix[F[ ]](in: F[Fix[F]])
El tipo Fix est´a paramertizado con otro constructor de tipo, que a su vez es pa- ram´etrico. Se puede ver al tipo Fix como un constructor que toma como par´ametro un constructor polim´orfico de tipos. El constructorFest´a parametrizado con un tipo que corresponde al de las expresiones de los constructores internos [16].
Tambi´en vamos a definir un sin´onimo de tipo y algunas funciones ´utiles:
typeTermInt = Fix[Term]
defnum = (n:Int) =>Fix[Term](Num(n))
defplus = (l:TermInt, r:TermInt) =>Fix[Term](Plus(l,r))
Las funciones num () y plus() sirven para construir expresiones de tipo Fix de manera mas sencilla, extract() sirve para acceder al constructor que parametriza a Fix. As´ı, podemos crear expresiones del tipo:
valn=plus(num(3),num(4))
Y podemos definir funciones sobre el tipo que utilizan pattern matching:
defcaso(t: TermInt) = extract(t)match{
caseNum(n) =>println(”Es Num!”)
casePlus(l,r) =>println(”Es Plus!”)
}
En su definici´on, el constructor Num no utiliza su par´ametro de tipo, ya que no contiene subexpresiones, s´olo contiene un n´umero entero [16].
Con todas las definiciones anteriores, surge la pregunta ¿Para qu´e redefinir nuestro tipoTermas´ı? Para poder declarar los constructores comofunctoresy as´ı generalizar el fold. Un constructor de tipos F es un functor si posee una funci´on fmap con signatura de tipo:
(A =>B) =>F[A] =>F[B] Y verifica las propiedades:
fmap(id) = id
fmap(f) compose fmap(g) = fmap(f compose g )
Un Functor puede pensarse como una estructura que puede ser mapeada. Si tengo una estructura de tipoF[A]y una funci´onA =>B,fmapme devuelve otra estructura de tipo F[B]. La funci´on fmap es una generalizaci´on de la funci´on map para listas. En Haskell existe una clase de tipos que representa a los functores, la misma est´a de- finida de la siguiente manera:
class Functorfwhere
fmap :: (a−>b)−>f a−>f b
Debemos instanciar los constructores de un tipo algebraico, definiendo la funci´on
fmap para cada uno de ellos. En Scala no existen las clases de tipos como en Haskell, tales requerimientos podemos expresarlos como untrait que declara la funci´onfmap, haciendo que los constructores del tipo implementen esetrait. Esta codificaci´on tiene un estilo m´as orientado a objetos.
Definimos eltrait:
trait Functor[F[ ]] {
deffmap[A, B](f: A =>B)(r: F[A]): F[B]
Y hacemos que nuestro tipoTermimplemente el traitFunctor, es decir, cada cons- tructor del tipoTermva a tener que implementar un m´etodofmap, el c´odigo nos queda:
abstract classTerm[A]extendsFunctor[Term]
case classNum[A](value: Int)extendsTerm[A]{
override deffmap[A, B](f: A =>B)(t: Term[A]): Num[B] = tmatch{
caseNum(n) =>Num(n)
} }
case classPlus[A](left: A, right:A)extendsTerm[A]{
override deffmap[A , B](f: A =>B)(t: Term[A]): Plus[B] = tmatch{
casePlus(l,r) =>Plus(f(l), f(r))
} }
Ahora, con todas las piezas en su lugar, podemos definir la funci´on fold gen´erica para nuestro tipoTerm:
deffoldTerm[A](f: Term[A] =>A)(t: Fix[Term]): A = tmatch{
case(Fix(in)) =>f(in.fmap(foldTerm[A](f))(in))
}
En la nueva definici´on de foldTermno se utiliza pattern matching sobre los cons- tructores, la funci´on est´a definida sobre Term, cuyos constructores est´an definidos como Functores.
Con foldTermpodemos definir las funciones sobre el tipo, para lo cual encapsulamos las funciones que parametrizar´an a foldTermen una clase, para hacerlas extensibles:
classfoldParam{
defevalParam(t: Term[Int]):Int = tmatch{
caseNum(n) =>n
casePlus(l,r) =>l + r
} }
valparam =newfoldParam
defeval = foldTerm(param.evalParam)
Ahora si queremos extender el tipo algebraico, agregamos el nuevo caso, definien- do el m´etodo fmap y extendemos las funciones que parametrizan a foldTerm como sigue:
override deffmap[A , B](f: A =>B)(t: Term[A]): Mul[B] = tmatch{
caseMul(l,r) =>Mul(f(l), f(r))
} }
defmul = (l:TermInt, r:TermInt) =>Fix[Term](Mul(l,r))
classfoldParamExtended1extendsfoldParam{
override defevalParam(t: Term[Int]): Int = t match{
caseMul(l,r) =>l∗r
casedefault =>super.evalParam(default)
} }
valparamExtended =newfoldParamExtended1
defevalExt = foldTerm(paramExtended.evalParam)
De esta manera reutilizamos el c´odigo de las funciones que parametrizan afoldTerm() y la funci´on evalExt() utiliza la misma funci´on foldTerm() que se utiliz´o para definir eval().
Con esta nueva definici´on del tipo Termutilizando un punto fijo, tambi´en podemos definir y extender funciones sin utilizarfoldTerm()siguiendo el esquema mostrado en el cap´ıtulo 3, por ejemplo definimos la funci´on termtoString() sin utilizar fold y la extendemos:
classOp{
deftermtoString(t: TermInt): String = extract(t)match{
caseNum(n) =>n.toString
casePlus(l,r) =>”(”+termtoString(l)+”+”+termtoString(r)+”)”
} }
La extensi´on para manejar el casoMul:
classOpExtendedextendsOp{
override deftermtoString(t: TermInt): String = extract(t)match{
caseMul(l,r) =>”(”+termtoString(l)+”∗”+termtoString(r)+”)”
casedefault =>super.termtoString(t)
} }
Aquella persona con una fuerte influencia de la programaci´on orientada a ob- jetos argumentar´ıa que la codificaci´on de la funci´on foldTerm() es poco elegante y redundante. El “problema” est´a en el llamadoin.fmap(in, foldTerm[A](f)),fmap es una funci´on embebida (m´etodo) del objeto in, que se le pasa como par´ametro el mismo objetoin. Este pasaje no es necesario, se hizo de esta manera para quefmaptenga una signatura de tipos similar a la que se define en la clase de tiposFunctor de Haskell. Siendo Scala un lenguaje que tambi´en es orientado a objetos, podemos acercar la
definici´on de fmap a ese paradigma para evitar ese pasaje de par´ametros, el c´odigo quedar´ıa:
trait Functor[A,F[ ]] {
this: F[A] =>
deffmaps[B](f: A =>B)(r: F[A]): F[B]
final deffmap[B](f: A =>B):F[B] = fmaps(f)(this)
}
abstract classTerm[A]extendsFunctor[A, Term]
case classNum[A](value: Int)extendsTerm[A]{
override deffmaps[B](f: A =>B)(t: Term[A]): Num[B] = tmatch{
caseNum(n) =>Num(n)
} }
case classPlus[A](left: A, right:A)extendsTerm[A]{
override deffmaps[B](f: A =>B)(t: Term[A]): Plus[B] = tmatch{
casePlus(l,r) =>Plus(f(l), f(r))
} }
Y la definici´on de foldTermse compactar´ıa un poco:
deffoldTerm[A](f: Term[A] =>A)(t: Fix[Term]): A = tmatch{
case(Fix(in)) =>f(in.fmap(foldTerm[A](f)))
}