开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜