Monday, May 14, 2012

Implementación funcional del tres en raya en Scala

Aquí va una implementación del tres en raya, o tic-tac-toe, en Scala. Mi nueva contribución a Rosetta Code. El juego es interactivo humano-computador. Comienza el humano. El computador no es muy listo: hace movimientos legales pero puramente aleatorios. La implementación es funcional. El listado completo lo tenéis en Rosetta Code. Voy a comentar aquí algunos de sus elementos más interesantes.
  • val BaseBoard = ('1' to '9').toList Esto define una variable inmutable que contiene una Lista de caracteres entre el 1 y el 9, que serán nuestro tablero base. Representaremos un tablero como una Lista de caracteres.
123
456
789
    • '1' to '9' define un rango de caracteres del '1' al '9'. 1 to 9 definiría un rango de enteros entre el 1 y el 9. 1 until 9 definiría un rango de enteros entre el 1 y el 8
  • val WinnerLines = List((0,1,2), (3,4,5),...,(2,4,6)) En WinnerLines guardamos una Lista de Tuplas, donde cada Tupla contiene uno de los movimientos ganadores (líneas completas). 0,1,2 corresponde a  la primera línea horizontal (ojo, son los índices de las posiciones que corresponden a la primera línea horizontal), mientras que por ejemplo 2,4,6 se corresponde a una de las dos diagonales.
  • class Board(aBoard : List[Char] = BaseBoard) La clase Board representará nuestro tablero y nos permitirá instanciar objetos funcionales (que no guardan estado mutable). Esta clase tiene un constructor que recibe como parámetro una Lista de caracteres. Si no se le pasa parámetro, esa Lista de caracteres será por defecto BaseBoard. El parámetro del constructor (aBoard) automáticamente pasa a ser accesible por las funciones de la clase, aunque no lo es desde fuera.
  • def availableMoves = aBoard.filter(c => c != Human && c != Computer) 
    • Definimos una función de la clase Board que no tiene parámetros, y pero eso no es necesario poner paréntesis después del nombre (aunque podríamos hacerlo; def availableMoves() = ...).
    • Esta función devuelve una Lista de caracteres. Scala es un lenguaje tipado estáticamente, pero el compilador puede deducir muchas veces los tipos de variables y funciones y en estas ocasiones no es necesario decírselos explícitamente. Aunque el compilador lo deduzca, si queremos podemos hacerlo explícito (puede ser una buena documentación): def availableMoves : List[Char] = ...
    • El cuerpo de la función es una llamada a la función filter sobre el objeto aBoard (que es una List[Char]). Sobre una Lista, filter toma como parámetro una función que devuelve un booleano, y lo aplica a cada elemento de la Lista; luego devuelve una nueva Lista en la que sólo se copian aquellos elementos de la original para las que el filtro es cierto. 
    • c => c!= Human && c!= Computer es una función anónima. Toma como parámetro un carácter que llamamos c y devuelve cierto si es distinto de Human y es distinto de Computer (Human y Computer son dos constantes que están definidas como 'X' y 'O'). En este caso Scala puede deducir que c es un Char porque se usa para filtrar una Lista de caracteres. También podríamos ponerlo explícito: (c : Char) => ... Si el parámetro sólo se usara una vez en el cuerpo de la función anónima, ni siquiera habría que darle un nombre. Por ejemplo si sólo queremos quedarnos los elementos distintos de Human: aBoard.filter(_ != Human) es todo lo que necesitaríamos poner.
    • Un ejemplo: si aBoard es List('1','2','X','O'), availableMoves nos devolverá List('1','2')
  • def availableMovesIdxs = for ((c,i) <- aBoard.zipWithIndex if c != Human && c != Computer) yield i Esta función usa una list comprehension. El for ... yield nos permite crear una lista a partir de otra, u otras, con bastante flexibilidad. 
    • En nuestro ejemplo, lo primero es coger aBoard (List[Char]) y llamar a zipWithIndex, que nos devuelve una Lista de pares ([Char], [Int]) en la que se asocia a cada elemento de una Lista su posición en la misma: por ejemplo, List('a','b').zipWithIndex nos devolvería List(('a',0), ('b',1)). 
    • Luego, para aquellos pares que cumplan la condición de que el carácter (c) no es ni Human ni Computer, nos quedaremos con el índice (i). Por ejemplo, si aBoard es List('1','2','X','O'), availableMovesIdxs nos devolverá List(0,1).
  • def computerPlays = new Board(aBoard.updated(availableMovesIdxs(randomGen.nextInt(availableMovesIdxs.length)), Computer)) Esta función devuelve un nuevo objeto Board, después de que el computador haga un movimiento aleatorio. 
    • Primero, hay que hacer notar que este estilo es característico de los objetos funcionales: en lugar de modificar el estado del objeto Board actual, instanciamos uno nuevo con los cambios que queramos. De esta forma podemos trabajar sin tener estado mutable (por ejemplo, sin tener variables en el sentido imperativo de la palabra variable).
    • Usamos la función updated sobre la Lista de caracteres aBoard para crear una copia de esta Lista donde el elemento en la posición que le pasamos como primer parámetro se cambia por el segundo parámetro (Computer en este caso). El primer parámetro availableMovesIdxs(randomGen.nextInt(availableMovesIdxs.length)) selecciona aleatoriamente un elemento de entre availableMovesIdxs. Notad que availableMovesIdxs es una función que no tiene parámetros y aquí parece que le estamos pasando uno. No es verdad, availableMovesIdxs devuelve una Lista, así que el "parámetro" en realidad es el acceso al elemento i-ésimo de esa Lista. Notad que en este caso no podríamos escribir availableMovesIdxs()(0) porque el compilador protestaría porque intentamos acceder a un elemento de una Lista sin especificar cual.
  •   def isWinner(winner : Char) =
        WinnerMoves.exists{case (i,j,k) => aBoard(i) == winner && aBoard(j) == winner && aBoard(k) == winner}
    Esta función determina si el carácter pasado como parámetro (que debería ser 'O' o 'X' aunque no lo estamos comprobando) hace una línea completa en aBoard. Para ello usamos la Lista de Tuplas WinnerMoves donde teníamos guardados los índices de todas las líneas completas posibles en el tablero, y usamos la función exists, que toma como parámetro una función que devuelve un booleano y la aplica a cada elemento de la Lista. Si para algún elemento la función parámetro devuelve cierto, exists termina y devuelve cierto.
    • La función que le pasamos como parámetro tiene una sintaxis un tanto especial. Lo primero es que va entre llaves, en lugar de paréntesis. Y segundo, empieza con la palabra reservada case. Es otro ejemplo de lo que se puede hacer con emparejamiento de patrones en Scala: definir funciones parciales (que sólo devuelven un valor para ciertas entradas). En nuestro caso, sabemos con certeza que todos los elementos de la lista WinnerMoves son 3-tuplas y que todas tienen tres caracteres, así que podemos emparejar con confianza con un patrón que sólo acepte 3-tuplas y comparar sus tres elementos con el parámetro recibido. Si alguna de las 3-tuplas de WinnerMoves tiene todo 'X' o todo 'O' en nuestro tablero, tenemos un ganador.
    • Si queremos mayor seguridad, podemos asegurarnos de que lo que recibe la función es 'O' o 'X'. Para eso añadiríamos require(winner == 'X' || winner == 'O') como primera línea de la función. Esto nos permite añadir fácilmente precondiciones a nuestras funciones: si no se cumple la precondición, la función termina con una excepción de tipo IllegalArgumentException, y si se cumple, se ejecuta normalmente.
  •   def print {
        aBoard.grouped(3).foreach(row => println(row(0) + " " + row(1) + " " + row(2)))
      }
    Aquí tenemos un procedimiento. Notad que no lleva = después del nombre, directamente va el cuerpo entre llaves. En realidad un procedimiento en Scala es una función que devuelve algo de tipo Unit (o sea, que devuelve algo sin interés; Unit es similar al void de Java). En este caso print no devuelve ningún valor de interés, lo que hace es escribir por pantalla el tablero. Primero agrupa la Lista aBoard en grupos de tres, y luego para cada grupo de tres lo escribe en una línea en pantalla.
  • object TicTacToe extends Application {   En Scala podemos crear objetos singleton, que sólo se instancian una vez, con la palabra reservada object. Si creamos uno que extiende el trait (los traits para otro día) Application, el objeto será directamente ejecutable (el trait Application define automáticamente un método main que instancia el objeto singleton que acabamos de crear y ejecuta el código que lleve).
  • def readValidMove() : Char = { Una última cosa que quiero mencionar, y esta función me sirve como ejemplo (no copio aquí el código completo, ya está en Rosetta Code). Tenemos que leer la entrada del usuario de teclado hasta que nos proporcione un movimiento válido. Si creamos un bucle (algo tipo while(!validMove)...) vamos a necesitar leer la entrada del usuario en una variable mutable posiblemente varias veces. Las variables mutable en Scala se declaran con la palabra reservada var (val es para las inmutables). Sin embargo las variables mutables no son funcionales, así que en lugar de un bucle he implementado esto como una función recursiva, y así puedo usar val en lugar de var. En este caso creo que sería perfectamente razonable usar var y un bucle para esta parte del código, y Scala nos da la opción de hacerlo así, pero he preferido mantener un estilo funcional en todo el ejemplo. Lo mismo he hecho con la función play.
    • Por si no queda claro, esto es perfectamente legal en Scala: var a = 5; a = 6;  y hace lo que un programador acostumbrado a un lenguaje imperativo esperaría. Esto sin embargo es ilegal: val a = 5; a = 6; y nos devulve un error.
Para terminar, y por si acaso alguien lo nota, voy a señalar que la entrada/salida en este programa no es "puramente" funcional. Lo que busco es explorar y mostrar las posibilidades que Scala ofrece de combinar programación funcional y orientada a objetos sobre la plataforma Java. Scala también permite programar en un estilo totalmente imperativo, o mezclando el estilo imperativo y el funcional. El que busque programación funcional "pura" debería aprender Haskell.

No comments: