Scala, represent pattern of boolean tuple into something else
This is a cellular automata rule (input Boolean == Left, Center, Right Cell) and output Boolean. What is a better way to represent this in Scala?
trait Rule {
def ruleId() : Int
def rule(inputState:(Boolean, Boolean, Boolean)) : Boolean
override def toString : String = "Rule:" + ruleId
}
class Rule90 extends Rule {
def ruleId() = 90
def rule(inputState开发者_如何学Go:(Boolean, Boolean, Boolean)) : Boolean = {
// Verbose version, show all 8 states
inputState match {
case (true, true, true) => false
case (true, false, true) => false
case (false, true, false) => false
case (false, false, false) => false
case _ => true
}
}
}
One observation... In typical use, you're going to find that the sliding
method is the easiest way to feed data into your automata. It works something like this:
scala> val input = Seq(1,2,3,4,5,6,7,8,9)
input: Seq[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)
scala> input.sliding(3).toList
res4: List[Seq[Int]] =List(
List(1, 2, 3),
List(2, 3, 4),
...
List(6, 7, 8),
List(7, 8, 9))
To ensure that the number of sequences output by sliding
is equal to the number of input elements, you need to pad the input sequence on either side:
scala> (0 +: input :+ 0).sliding(3).toList
res9: List[Seq[Int]] = List(
List(0, 1, 2),
List(1, 2, 3),
...
List(7, 8, 9),
List(8, 9, 0))
Enough of the theory then, back to the code!
For your example (and because I understand something of the underlying problem) I'm padding the sequence with false
values here.
Because sliding
will output sequences and not tuples, I created the seqYieldsTrue
helper method to handle the translation. I also renamed rule
to apply
, so that your class can be directly used as a function:
trait Rule {
def ruleId: Int //abstract
def yieldsTrue(input: (Boolean,Boolean,Boolean)): Boolean //abstract
override def toString: String = "Rule:" + ruleId
private def seqYieldsTrue(input: Seq[Boolean]) = {
assert(input.size == 3)
input match {
case Seq(a,b,c) => yieldsTrue((a,b,c))
case _ => error("invalid input size")
}
}
def apply(input: Seq[Boolean]) =
(false +: input :+ false).sliding(3) map { seqYieldsTrue }
}
class Rule90 extends Rule {
val ruleId = 90
val falsehoods = Seq(
(true, true, true),
(true, false, true),
(false, true, false),
(false, false, false)
)
def yieldsTrue(input: (Boolean,Boolean,Boolean)) = !falsehoods.contains(input)
}
Then again, I did say I understood the underlying problem! So lets just do away with all these tedious manual rule definitions, and let the compiler generate the lot for us :)
If you don't mind some bit-twiddling...
class WolframAutomata(val ruleId: Int) extends Rule {
def pow2(x: Int) = math.pow(2,x).toInt
def isBitSet(x: Int, bit: Int) = (x & pow2(bit)) > 0
// 8 possible input patterns corresponding to
// different combinations of 3 input bits
val inputs = (0 to 7) map { id =>
Tuple3(
isBitSet(id, 2),
isBitSet(id, 1),
isBitSet(id, 0)
) -> id
} toMap
//each of the 8 input rules corresponds to one bit in the ruleId
val outputs = inputs mapValues { isBitSet(ruleId, _) }
def yieldsTrue(input: (Boolean,Boolean,Boolean)) = outputs(input)
}
(the rule for generating automata from IDs taken from here: http://www.wolframscience.com/nksonline/page-53#previous)
Working like this, you can also roll the logic back up into the Rule trait, as there's little need for a separate abstract trait if there'll only ever be one subclass. You could also safely do away with yieldsTrue
in this case and just work directly with the output
val. I'll leave that as an exercise to the reader...
Putting it all together (useless REPL output lines removed):
scala> val r90 = new WolframAutomata(90)
r90: WolframAutomata = Rule:90
scala> def destringify(s:String) = s map { case 'X' => true; case _ => false } toSeq
scala> val input = destringify(".......X.......")
scala> val output = Iterator.iterate(input)(r90.apply(_).toSeq) toSeq
scala> def stringify(xs: Seq[Boolean]) = xs map {case true => "X"; case _ => "."} mkString
//can you see what it is yet?
scala> val sierpinski = output.take(10).map(stringify).mkString("\n")
sierpinski: String =
.......X.......
......X.X......
.....X...X.....
....X.X.X.X....
...X.......X...
..X.X.....X.X..
.X...X...X...X.
X.X.X.X.X.X.X.X
...............
...............
Please forgive all the toSeq
calls, they're mostly to force evaluation so that you can see some actual output on the REPL, and not just non-empty iterator
Instead of
inputState match {
case (true, true, true) => false
case (true, false, true) => false
case (false, true, false) => false
case (false, false, false) => false
case _ => true
}
you could say
inputState match {
case (true, _, true) | (false, _, false) => false
case _ => true
}
or, simply but maybe not as clearly (depending on purpose/context)
def rule(inputState:(Boolean, Boolean, Boolean)) = inputState._1 != inputState._3
The other answers focused on how to optimize the pattern matching to make it shorter. However, I think that Rule90
might be only an example here. Maybe your question is not how to optimize the pattern matching of rule 90, but if there is a more appropriate way to define the rule type with Scala constructs. Here is my take on that.
First, I wouldn't recommend making subclasses for different rules. You should have one single Rule
class, and all the concrete rules would be instances of it. Here would be my definition of the Rule
class:
case class Rule(val id:Int, f:PartialFunction[(Boolean,Boolean,Boolean),Boolean])
extends (((Boolean,Boolean,Boolean))=>Boolean) {
def apply(state:(Boolean,Boolean,Boolean)) = f(state)
}
Now, the Rule
class is also a proper Scala function. You can instantiate it quite easily, like this:
val rule90 = Rule(90, {
case (true, true, true) => false
case (true, false, true) => false
case (false, true, false) => false
case (false, false, false) => false
case _ => true
})
That's why I chose f
to be a PartialFunction
, so we can use the Scala syntax for defining partial functions without any overhead, just like this. The disadvantage is that the compiler won't complain in case the match is not exhaustive. You need to make up your mind which is more important to you: code brevity or exhaustive error checking.
If the latter is the case, you can change f
to be a Function
, by declaring its type as ((Boolean,Boolean,Boolean))=>Boolean
. In that case, you would need to declare a specific rule by passing f
as a closure to the constructor.
You can apply the Rule
function very easily:
val state = (true, false, true)
val newState = rule90(state) // should be false
Also, there are more tricks you can do. Let's say you have all your rules inside a list:
val ruleList = List(rule01, rule02, rule03 /* .... */)
Then, you can for example make a mapping from rule id to rule instance like this:
val rules = ruleList.map(r=>(r.id, r)).toMap
And you could access and use a rule like that:
val state = (true, false, true)
val newState = rules(90)(state)
Or, in order to get all of the next states for all rules:
val state = (true, false, true)
val newStates = rules mapValues _(state)
And access one of the results like this:
val newState_90 = newStates(90)
Pretty cool. However, you can do most of this also with your original Rule
definition. I just wanted to play with the idea a little, because I love cellular automata.
You can use extractors to put meaning on the input state values
object EqualLeftAndRight {
def unapply(inputState:(Boolean, Boolean, Boolean)) = inputState._1 == inputState._3
}
def rule(inputState:(Boolean, Boolean, Boolean)) : Boolean =
inputState match {
case EqualLeftAndRight() => true
case _ => false
}
Also, to extend on @Madoc's answer, since you pass a partial function, it doesn't need to cover all cases:
case class Rule(val id:Int)(f:PartialFunction[(Boolean,Boolean,Boolean),Boolean]) extends (((Boolean,Boolean,Boolean))=>Boolean) {
def apply(state:(Boolean,Boolean,Boolean)) = if (f.isDefinedAt(state)) f(state) else false
}
val rule90 = Rule(90) {
case EqualLeftAndRight() => true
}
One additional point, if your ruleIds are meant to be constants, then you can (and should) declare them as vals rather than null-arg defs. Like your defs, the vals defined can be abstract in the trait, and concrete in the class.
trait Rule {
val ruleId : Int
}
class Rule90 extends Rule {
val ruleId= 90
}
Simplified further...
Welcome to Scala version 2.8.0.final (Java HotSpot(TM) Client VM, Java 1.6.0_21).
Type in expressions to have them evaluated.
Type :help for more information.
scala> def rule(inputState:(Boolean, Boolean, Boolean)) = inputState._1 != inputState._3
rule: (inputState: (Boolean, Boolean, Boolean))Boolean
scala> rule((true, false, true))
res0: Boolean = false
scala> rule((true, false, false))
res1: Boolean = true
scala> rule((false, false, false))
res2: Boolean = false
scala> rule((false, true, false))
res3: Boolean = false
scala> rule((false, true, true))
res4: Boolean = true
Oops, sorry, I see @Debilski has this already.
精彩评论