Пример функционального программирования в scala
Я изучаю scala. Это очень многообещающе, благодаря Одерскому и всем остальным авторам за их большую работу.
Я взял проблему эйлера (http://projecteuler.net/), чтобы иметь более-минимальный пример. И я пытаюсь идти по функциональному пути. Так что это не "пожалуйста, ответьте мне немедленно или мой босс убьет меня", но "пожалуйста, если у вас есть время, можете ли вы помочь императивному языковому программисту совершить путешествие в функциональном мире?"
Проблема: я хочу класс для рук в покере. Покерная рука состоит из нескольких карт, от 0 до 5. Я хотел бы создать список карт один и для всех, то есть: мой класс Руки будет неизменным, если я хочу добавить карту, то Я создаю новый объект Hand.
Поэтому мне нужна коллекция Card, которую можно создать как "val", а не var.
Первый шаг: конструкторы, по одному для каждого числа карт. Но сборка карт обрабатывается в каждом конструкторе, поэтому я должен иметь его как var!
Здесь код, класс карты - это просто Костюм и значение, переданные конструктору в виде строки ( "5S" - это 5 пиков):
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 */
}
Вы знаете, как сделать его истинным функциональным способом?
Спасибо.
PS: если вы действительно хотите знать, это проблема 54.
Ответы
Ответ 1
Мой ответ не о функциональном аспекте scala, но ваш код можно быстро написать с помощью 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*
в вспомогательном конструкторе говорит, что вы можете иметь переменное количество аргументов (даже тысячи строк).
Я получаю ввод и вызываю создание для каждой новой Карты с помощью функции map
, а затем передаю результат родительскому конструктору, у которого есть собственное требование.
(BTW, отображение из строки в Карту можно сделать анонимно, таким образом: this(input.map(new Card(_)).toList)
)
class Hand(val mycards: List[Card]) {...
Подходит для
class Hand(cards: List[Card]) {
val mycards = cards
...
С этого момента, если вы попытаетесь создать более пяти карт в руках, вы получите 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)
Ответ 2
Ну, var
в коде ниже проистекает из инициализации cards
из основного конструктора:
// 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)
}
Проблема проста: вам нужен список карт, образованных ненулевыми строками. Если бы я был вами, я бы просто избегал передачи нулей, но... В любом случае, лучший способ справиться с этим - это преобразовать это в параметры. Преобразование простое: Option(a)
вернется Some(a)
is a
не равно null, а None
, если оно есть. Если вы составите список этого, вы можете flatten
удалить его None
и преобразовать Some(a)
обратно в a
. Другими словами:
def this(a : String, b : String, c : String, d : String, e : String) =
this(List(a, b, c, d, e).map(Option(_)).flatten.map(Card(_)))
Ответ 3
Потому что в этом примере вам разрешено использовать только пять карт, которые я проверил бы во время компиляции с помощью 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) }
Конечно, в другом примере, когда может быть неспецифическое количество элементов, вам нужно проверить их во время выполнения. productIterator
возвращает только Iterator [Any], но когда вы используете свои карты непосредственно с помощью идентификаторов полей (_1.. _5), вы получите ACard
.
Ответ 4
Во-первых, null
является злом, вместо этого используйте Option
. Во-вторых, Scala поддерживает параметры по умолчанию. Поэтому вместо создания всех конструкторов вы можете просто использовать один из них следующим образом:
def this(a: String = null, ..., e: String = null) = ...
или Option
, что более безопасно.
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)
}
Таким образом, карты добавляются только в буфер, если они "существуют".
Ответ 5
Во-первых, нам нужно исправить ошибку компиляции в определении поля cards
.
Обратите внимание, что в Scala вам обычно не нужно объявлять поля. Основными параметрами конструктора уже являются поля! Таким образом, это можно записать проще:
class Hand(cards : List[Card]) {
if (cards.length>5)
throw new IllegalArgumentException(
"Illegal number of cards: " + mycards.length);
Теперь у нас есть проблема мутируемости. Если вы хотите программировать в функциональном стиле, все должно быть неизменным, поэтому "конструктор полной руки" вообще не работает: он имеет 6 побочных операций, последний из которых не компилируется.
В функциональной настройке объект не может быть изменен после завершения его конструктора, поэтому весь код после this(Nil)
бесполезен. Вы уже сказали, что cards
есть Nil
, что еще вы хотите?! Итак, все вычисления должны произойти до вызова главного конструктора. Мы хотели бы удалить this(Nil)
сверху и добавить this(sortCards(cardBuffer.toList))
в конец. К сожалению, Scala не позволяет этого. К счастью, это позволяет больше возможностей достичь того же, чем java: во-первых, вы можете использовать вложенный блок следующим образом:
this({
val cardBuffer = ... /* terrible imperativeness */
sortCards(cardBuffer.toList)
})
second, вы можете использовать метод apply
вместо конструктора:
object Hand {
def apply(a : String, b : String, c : String, d : String, e : String) = {
val cardBuffer = ... /* terrible imperativeness */
new Hand(sortCards(cardBuffer.toList))
}
}
Теперь давайте начнем избавляться от императива ListBuffer
. Первым улучшением будет использование var
типа List[Card]
. Создание изменчивости более локально поможет удалить его позже:
// 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)
Хорошо, теперь мы можем видеть, что именно мы мутируем и можем легко удалить эту изменчивость:
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)
Теперь у нас есть достаточное количество дубликатов кода. Удалите это с помощью грубой силы (найдите длинный дублирующий фрагмент кода и функцию извлечения)!
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)
Далее, очень важно, преобразование было бы заменой нулевых ссылок значениями типа Option
или вообще удалить концепцию пустой карты.
Update:
В соответствии с запросом я добавляю пример использования метода apply
. Обратите внимание, что он объявлен в object Hand
, а не class Hand
, поэтому ему не нужен экземпляр класса (он похож на статический метод в java). Мы просто применяем объект к параметрам: val hand = Hand("5S", "5S", "5S", "5S", "5S")
.
Ответ 6
Попытка использования varargs, overloaded + operator, повторное добавление в конструкторе и Set для устранения дубликатов карт.
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)
}
}