programming - Un ejemplo de programación funcional en scala
scala programming (6)
Estoy estudiando Scala. Es muy prometedor, gracias a Odersky y a todos los demás autores por su gran trabajo.
Tomé un problema de euler ( http://projecteuler.net/ ) para tener un ejemplo más-entonces-mínimo. Y estoy tratando de seguir el camino funcional. Entonces, este no es un "por favor respóndeme inmediatamente o mi jefe me matará", sino un "por favor, si tienes tiempo, ¿puedes ayudar a un programador de idiomas imprescindible a hacer un viaje en el mundo funcional?"
Problema: quiero una clase para manos de póker. Una Mano de póker está compuesta por una cantidad de cartas, de 0 a 5. Me gustaría construir la lista de cartas de una y para todas, es decir: mi clase de Mano será inmutable, si quiero agregar una carta, entonces Creo un nuevo objeto Mano. Entonces necesito una colección de tarjetas que se pueda crear como "val", no como var. Primer paso: constructores, uno para cada número de tarjetas. Pero la colección de Card se maneja en cada constructor, ¡así que debo tenerla como var!
Aquí está el código, la clase de la Tarjeta es simplemente un Traje y un Valor, pasado al constructor como una cadena ("5S" es el 5 de las espadas):
class Hand(mycards : List[Card]) {
// this should be val, I guess
private var cards : List[Card] = {
if (mycards.length>5)
throw new IllegalArgumentException(
"Illegal number of cards: " + mycards.length);
sortCards(mycards)
}
// full hand constructor
def this(a : String, b : String, c : String, d : String, e : String) = {
this(Nil)
// assign cards
val cardBuffer = new ListBuffer[Card]()
if ( a!=null ) cardBuffer += new Card(a)
if ( b!=null ) cardBuffer += new Card(b)
if ( c!=null ) cardBuffer += new Card(c)
if ( d!=null ) cardBuffer += new Card(d)
if ( e!=null ) cardBuffer += new Card(e)
cards = sortCards(cardBuffer.toList)
}
// hand with less then 5 cards
def this(a : String, b : String, c : String, d : String) = this(a,b,c,d,null)
def this(a : String, b : String, c : String) = this(a, b, c, null)
def this(a : String, b : String) = this(a, b, null)
def this(a : String) = this(a, null)
def this() = this(Nil)
/* removed */
}
¿Sabes cómo hacer que sea la verdadera forma funcional? Gracias.
PD: si realmente quieres saber, es un problema 54.
Bueno, la var
en el siguiente código proviene de que usted no inicializa las cards
del constructor principal:
// this should be val, I guess
private var cards : List[Card] = {
if (mycards.length>5)
throw new IllegalArgumentException(
"Illegal number of cards: " + mycards.length);
sortCards(mycards)
}
Entonces, lo que tienes que hacer es arreglar el constructor secundario:
// full hand constructor
def this(a : String, b : String, c : String, d : String, e : String) = {
this(Nil)
// assign cards
val cardBuffer = new ListBuffer[Card]()
if ( a!=null ) cardBuffer += new Card(a)
if ( b!=null ) cardBuffer += new Card(b)
if ( c!=null ) cardBuffer += new Card(c)
if ( d!=null ) cardBuffer += new Card(d)
if ( e!=null ) cardBuffer += new Card(e)
cards = sortCards(cardBuffer.toList)
}
El problema es simple: quieres una lista de cartas formadas por cadenas no nulas. Si yo fuera tú, simplemente evitaría pasar nulos, pero ... De todos modos, la mejor manera de manejar eso es convertir esto en opciones. La conversión es simple: la Option(a)
devolverá Some(a)
is a
is not null, y None
si lo es. Si compone una lista de eso, puede flatten
para eliminar el None
y convertir Some(a)
nuevamente en a
. En otras palabras:
def this(a : String, b : String, c : String, d : String, e : String) =
this(List(a, b, c, d, e).map(Option(_)).flatten.map(Card(_)))
Debido a que en este ejemplo solo se le permite usar cinco tarjetas, lo verificaría en tiempo de compilación con el uso de un Tuple5:
type HandType = (ACard, ACard, ACard, ACard, ACard)
case class Hand(h: HandType)
abstract class ACard {
def exists: Boolean
}
case class Card(value: Int, color: Color) extends ACard {
def exists = true
}
case object NoCard extends ACard {
def exists = false
}
abstract class Color(val c: Int)
case object H extends Color(1)
case object C extends Color(2)
case object S extends Color(3)
case object D extends Color(4)
case object NoColor extends Color(0)
implicit def tuple2Card(t: (Int, Color)) = Card(t._1, t._2)
val h1 = Hand((Card(4, H), Card(6, S), Card(2, S), Card(8, D), NoCard))
val h2 = Hand((4 -> H, 6 -> S, 2 -> S, 8 -> D, NoCard))
println(h1)
println(h2)
h1.h.productIterator foreach { c => println(c.asInstanceOf[ACard].exists) }
Por supuesto, en otro ejemplo, cuando puede haber una cantidad no específica de elementos, debe verificarlos en tiempo de ejecución. productIterator
solo devuelve un iterador [Cualquiera], pero cuando utiliza sus tarjetas directamente mediante los identificadores de campo (_1 .. _5) obtendrá un ACard
.
En primer lugar, null
es malo, use Option
lugar. En segundo lugar, Scala admite parámetros predeterminados. Entonces, en lugar de crear todos los constructores, quizás solo quieras usar uno de ellos como este:
def this(a: String = null, ..., e: String = null) = ...
o con Option
, que es más seguro.
def this(a: Option[String] = None, ..., e: Option[String] = None) = {
this(Nil)
val cardBuffer = new ListBuffer[Card]()
a foreach { cardBuffer += new Card(_) }
b foreach { cardBuffer += new Card(_) }
c foreach { cardBuffer += new Card(_) }
d foreach { cardBuffer += new Card(_) }
e foreach { cardBuffer += new Card(_) }
cards = sortCards(cardBuffer.toList)
}
Entonces las tarjetas solo se agregan al búfer si "existen".
Mi respuesta no es sobre el aspecto funcional de scala, pero su código es posible escribirlo en breve usando scala sugar:
class Hand(val mycards: List[Card]) {
require (mycards.size <= 5,"can''t be more than five cards")
def this(input: String*) = {
this(input.map(c => new Card(c)).toList)
}
}
input: String*
en el constructor auxiliar dice que puede tener un número variable de argumentos (incluso miles de cadenas). Obtengo la creación de entrada e invocación para cada nueva tarjeta con función de map
, y luego paso el resultado al constructor principal que tiene su propio requisito . (Por cierto, el mapeo de la cadena a la tarjeta se puede hacer de forma anónima, de esta manera: this(input.map(new Card(_)).toList)
)
class Hand(val mycards: List[Card]) {...
Es igual a
class Hand(cards: List[Card]) {
val mycards = cards
...
A partir de ahora, si intentas crear más de cinco cartas en la mano obtendrás java.lang.IllegalArgumentException
:
scala> class Card(s: String) {println("Im a :"+s)}
defined class Card
scala> new Hand("one","two","three","four","five","six")
Im a :one
Im a :two
Im a :three
Im a :four
Im a :five
Im a :six
java.lang.IllegalArgumentException: requirement failed: can''t be more than five card
at scala.Predef$.require(Predef.scala:157)
at Hand.<init>(<console>:9)
Primero, necesitamos arreglar un error de compilación en la definición de su campo de cards
.
Tenga en cuenta que en Scala generalmente no tiene que declarar campos. ¡Los parámetros principales del constructor ya son campos! Entonces, esto puede escribirse más simple:
class Hand(cards : List[Card]) {
if (cards.length>5)
throw new IllegalArgumentException(
"Illegal number of cards: " + mycards.length);
Ahora tenemos un problema de mutabilidad. Si desea programar en estilo funcional, todo debe ser inmutable, por lo que el "constructor de mano completo" no funciona en absoluto: tiene 6 operaciones de efecto secundario, la última de las cuales no se compila.
En la configuración funcional, un objeto no puede ser modificado después de que su constructor haya terminado, por lo que todo el código después de this(Nil)
es inútil. Ya dijiste que las cards
son Nil
, ¿qué otra cosa quieres? Entonces, todos los cálculos tienen que ocurrir antes de la llamada al constructor principal. Nos gustaría eliminar this(Nil)
de la parte superior y agregar this(sortCards(cardBuffer.toList))
al final. Desafortunadamente, Scala no permite eso. Afortunadamente, permite más opciones para lograr lo mismo que Java: primero, puede usar un bloque anidado como este:
this({
val cardBuffer = ... /* terrible imperativeness */
sortCards(cardBuffer.toList)
})
segundo, puede usar el método apply
lugar de un constructor:
object Hand {
def apply(a : String, b : String, c : String, d : String, e : String) = {
val cardBuffer = ... /* terrible imperativeness */
new Hand(sortCards(cardBuffer.toList))
}
}
Ahora, comencemos a deshacernos del imperativo ListBuffer
. La primera mejora sería usar var
de tipo List[Card]
. Hacer la mutabilidad más local ayudará a eliminarlo más tarde:
// assign cards
var cards = Nil
if ( e!=null ) cards = new Card(e) :: cards
if ( d!=null ) cards = new Card(d) :: cards
if ( c!=null ) cards = new Card(c) :: cards
if ( b!=null ) cards = new Card(b) :: cards
if ( a!=null ) cards = new Card(a) :: cards
sortCards(cards)
De acuerdo, ahora podemos ver exactamente qué estamos mutando y podemos eliminar fácilmente esa mutabilidad:
val fromE = if ( e!=null ) new Card(e) :: Nil else Nil
val fromD = if ( d!=null ) new Card(d) :: fromE else fromE
val fromC = if ( c!=null ) new Card(c) :: fromD else fromD
val fromB = if ( b!=null ) new Card(b) :: fromC else fromC
val fromA = if ( a!=null ) new Card(a) :: fromB else fromB
sortCards(fromA)
Ahora tenemos una buena cantidad de duplicación de código. Vamos a eliminar eso de una manera de fuerza bruta (¡busque una pieza larga de duplicación de código y extraiga la función)!
def prependCard(x : String, cards : List[Card]) =
if ( x!=null ) new Card(x) :: cards else cards
val cards = prependCard(a, prependCard(b,
prependCard(c, prependCard(d,
prependCard(e, Nil)
))
))
sortCards(cards)
A continuación, muy importante, la transformación sería reemplazar referencias anulables con los valores del tipo de Option
, o eliminar el concepto de tarjeta vacía por completo.
Actualizar:
Como solicité, estoy agregando un ejemplo de uso del método de apply
. Tenga en cuenta que se declara en el object Hand
, no en la class Hand
, por lo que no necesita una instancia de la clase (es similar al método estático en java). Simplemente aplicamos el objeto a los parámetros: val hand = Hand("5S", "5S", "5S", "5S", "5S")
.
Intento de usar varargs, operador + sobrecargado, adición repetida en el constructor y Set para eliminar tarjetas duplicadas.
package poker
class Hand(private val cards:Set[Card] = Set.empty[Card]) {
def + (card:Card) = {
val hand = new Hand(cards + card)
require(hand.length > length, "Card %s duplicated".format(card))
require(hand.length <= Hand.maxLength, "Hand length > %d".format(Hand.maxLength))
hand
}
def length = cards.size
override def toString = cards.mkString("(", ",", ")")
}
object Hand {
val maxLength = 5
def apply(cards:Card*):Hand = cards.foldLeft(Hand())(_ + _)
private def apply() = new Hand()
}
//-----------------------------------------------------------------------------------------------//
class Card private (override val toString:String)
object Card {
def apply(card:String) = {
require(cardMap.contains(card), "Card %s does not exist".format(card))
cardMap(card)
}
def cards = cardMap.values.toList
private val cardMap = {
val ranks = Range(2,9).inclusive.map { _.toString } ++ List("T", "J", "Q", "K", "A")
val suits = List("c","d","h","s")
(for(r <- ranks; s <- suits) yield (r + s -> new Card(r + s))).toMap
}
}
//-----------------------------------------------------------------------------------------------//
object Test extends App {
Array("1f", "Ac").foreach { s =>
try {
printf("Created card %s successfully/n",Card(s))
} catch {
case e:Exception => printf("Input string %s - %s /n", s, e.getMessage)
}
}
println
for(i <- 0 to 6) {
val cards = Card.cards.slice(0, i)
makeHand(cards)
}
println
val cards1 = List("Ac","Ad","Ac").map { Card(_) }
makeHand(cards1)
println
val hand1 = Hand(List("Ac","Ad").map { Card(_) }:_* )
val card = Card("Ah")
val hand2 = hand1 + card
printf("%s + %s = %s/n", hand1, card, hand2)
def makeHand(cards:List[Card]) =
try {
val hand = Hand(cards: _*)
printf("Created hand %s successfully/n",hand)
} catch {
case e:Exception => printf("Input %s - %s /n", cards, e.getMessage)
}
}