Extraktoren für Seq

Eine nützliche Funktion für Sequenzen ist indexOf, die die Position eines Elements innerhalb einer Sequenz bestimmt. Diese kann man in Extraktoren verpacken, z.B.:

object IndexOf {
   def unapply[T](pair:(Seq[T],T)):Option[Int] = {
     val index = pair._1.indexOf(pair._2)  
     if (index == -1) None else Some(index)
   }
}

object ReverseIndexOf {
   def unapply[T](pair:(Seq[T],T)):Option[Int] = {
     val index = pair._1.indexOf(pair._2)  
     if (index == -1) None else Some(pair._1.size - index - 1)
   }
} 

Nun kann man ein paar aus einer Sequenz und einem Element matchen:

def testIndexOf(n:Int) = (List(1,2,3,4,5,6,7),n) match {
  case IndexOf(0) => println("first")
  case IndexOf(1) => println("second")
  case ReverseIndexOf(1) => println("second to last")  
  case ReverseIndexOf(0) => println("last")  
  case IndexOf(x) => println("indexOf==" + x)
  case _ => println("not found")  
}

(1 to 8).foreach(testIndexOf(_))
//--> first
//--> second
//--> indexOf==2
//--> indexOf==3
//--> indexOf==4
//--> second to last
//--> last
//--> not found

In der vorliegenden Form ist das vielleicht noch nicht sooo furchtbar nützlich, aber das Prinzip lässt sich gut verallgemeinern. Ich finde es jedenfalls interessant, selbst Extraktoren zu schreiben, statt mich mit vorgefertigten oder durch Fallklassen autogenerierte Extraktoren zu begnügen.

Eigene Extraktoren

Heute nur ein paar Zeilen zu eigenen Extraktoren. Ich denke, einen guten Überblick über die verschiedenen Möglichkeiten gibt Jesse Eichar in seinem Daily Scala Blog. Ich will heute einen ganz einfachen Satz von Extraktoren vorstellen, mit denen man sich in vielen Fällen Guards sparen kann. Angenommen, wir haben folgenden Code:

case class Edge(from:String, to:String)

def makeEdge(p:(String,String)) = p match {
  case (from,to) if from != to => Edge(from, to)
  case _ => error("Can't create edge with identical end points")
}

Wäre es nicht schön, wenn wir einen Extraktor hätten, der auf Ungleichheit prüfen kann? Kein Problem:

object Ne {
   def unapply[T](pair:(T,T)):Option[(T,T)] = 
     if (pair._1 != pair._2) Some(pair) else None
}

def makeEdge(p:(String,String)) = p match {
  case Ne(from,to) => Edge(from, to)
  case _ => error("Can't create edge with identical end points")
}

Nach dem gleichen Schema lässt sich eine ganze Familie von Extraktoren basteln. Für Größenvergleiche benötigen wir zusätzlich einen impliziten Ordering-Parameter, und für den Gleichheits-Extraktor geben wir natürlich nur einen Wert zurück – zwei gleiche wären ja ziemlich witzlos. Hier die ganze Sammlung:

object Eq {
   def unapply[T](pair:(T,T)):Option[T] = 
      if (pair._1 == pair._2) Some(pair._1) else None
}

object Ne {
   def unapply[T](pair:(T,T)):Option[(T,T)] = 
     if (pair._1 != pair._2) Some(pair) else None
}

object Lt {
   def unapply[T](pair:(T,T))(implicit ord: Ordering[T]):Option[(T,T)] = 
     if (ord.lt(pair._1,pair._2)) Some(pair) else None
}

object Le {
   def unapply[T](pair:(T,T))(implicit ord: Ordering[T]):Option[(T,T)] = 
     if (ord.lteq(pair._1,pair._2)) Some(pair) else None
}

object Gt {
   def unapply[T](pair:(T,T))(implicit ord: Ordering[T]):Option[(T,T)] = 
     if (ord.gt(pair._1,pair._2)) Some(pair) else None
}

object Ge {
   def unapply[T](pair:(T,T))(implicit ord: Ordering[T]):Option[(T,T)] = 
     if (ord.gteq(pair._1,pair._2)) Some(pair) else None
}

Für eine Beschränkung habe ich leider noch keine Lösung gefunden, und zwar für Alternativen. Man kann zwar schreiben:

def g(p:(Int,Int)) = p match {
  case (10,20) | (20,10) => println("yes!")
  case _ => println("nope")
}

Aber es ist verboten, in den einzelnen Alternativen Variablen zu belegen, z.B.:

//doesn't work
def g(p:(Int,Int)) = p match {
  case (10,n) | (n,10) => println(n)
  case _ => println("nope")
}

Im Prinzip sollte das für den Compiler kein Problem sein, wenn in beiden Zweigen die gleiche Anzahl Variablen mit den gleichen Typen verwendet wird. Leider scheint es auch mit eigenen Extraktoren nicht möglich zu sein, das gewünschte Verhalten abzubilden. Oder hat jemand vielleicht eine zündende Idee?

So, ich denke das reicht für heute. Demnächst gibt es das Kohl-Ziege-Wolf-Puzzle in Haskell, aber das braucht noch etwas Feinschliff, vor allem der Kohl.