Thursday, November 19, 2009

Options

Scala is forced to have a "null" value because it interoperates with Java. However unlike Java APIs the recommended way to hand cases where there may or may not be a value (IE a return value) is to return an Option object. Compare the Scala and Java idioms for handling a possible null (or None) return value:

Note: The Option class have been updated in Scala 2.8 so I am going to use some of the scala 2.8 methods. Most of the examples will work with Scala 2.7 but not all; the principal is the same for 2.7 and 2.8.

Java:
  1. /**
  2.  * Returns the annotation with the given name or null if there is no annotation
  3.  * on the objects class with the given name.
  4.  */
  5. public static < A extends java.lang.annotation.Annotation> Annotation annotation(Object obj, Class< A> annotationCls) {
  6.   return obj.getClass().getAnnotation(annotationCls)
  7. }

Scala:
  1. import java.lang.annotation.Annotation
  2. /**
  3.  * Returns the annotation with the given name.
  4.  */
  5. def annotation[A <: Annotation](obj:Object, annotationCls:Class[A]) : Option[Annotation] = {
  6.    Option (obj.getClass.getAnnotation (annotationCls))
  7. }

In the Scala version it is obvious the use of the API (and the compiler) that there are two types of legal return types. Some or None and the compiler will force you to deal with this fact. Where in Java it is very easy to forget to check for null.

Also not that it is very easy to Wrap a null in an Option using the Option objects apply method. If the parameter is a null then None is return otherwise Some is returned.

The other important aspect is how to deal with the Option object and obtain the data from the object if it is present. This is both simple and non-simple. There are a large number of possible useage patterns that can be applied to it.

Examples:
  1. scala> import java.lang.annotation.Annotation
  2. import java.lang.annotation.Annotation
  3. kscala> def annotation[A <: Annotation](obj:Object, annotationCls:Class[A]) : Option[Annotation] = { 
  4.      |    Option (obj.getClass.getAnnotation (annotationCls))                                      
  5.      | }
  6. annotation: [A <: java.lang.annotation.Annotation](obj: java.lang.Object,annotationCls: Class[A])Option[java.lang.annotation.Annotation]
  7. // strings do not have the Documented annotation so None is returned
  8. scala> annotation("x", classOf[java.lang.annotation.Documented])                                   
  9. res0: Option[java.lang.annotation.Annotation] = None
  10. // None is not defined
  11. scala> res0.isDefined
  12. res1: Boolean = false
  13. // None IS empty 
  14. scala> res0.isEmpty
  15. res26: Boolean = true
  16. // We need a some example so LineNumberInputStream has Deprecated annotation
  17. // we will use that in order to 
  18. scala> val in = new LineNumberInputStream(new ByteArrayInputStream("hello".getBytes))
  19. in: java.io.LineNumberInputStream = java.io.LineNumberInputStream@8ca9a2d
  20. scala> annotation(in, classOf[Deprecated])                                           
  21. res2: Option[java.lang.annotation.Annotation] = Some(@java.lang.Deprecated())
  22. // Some(...) is always defined even if it contains null
  23. scala> res2.isDefined
  24. res3: Boolean = true
  25. scala> val nullSome = Some(null)
  26. nullSome: Some[Null] = Some(null)
  27. scala> nullSome.isDefined
  28. res28: Boolean = true
  29. // Some is never empty
  30. scala> res2.isEmpty
  31. res4: Boolean = false
  32. // You can also test particular values as follows
  33. scala> if(res0 == None) println("its ok to use")
  34. its ok to use
  35. scala> if (res2.map {a => "found"} == Some("found")) println ("it is deprecated dont use!")
  36. it is deprecated dont use!
  37. scala> res0 match {                                                       
  38.      | case None => println("its ok to use")                              
  39.      | case Some(x:Deprecated) => println ("it is deprecated dont use!"
  40.      | }
  41. its ok to use
  42. scala> res2 match {                                                      
  43.      | case None => println("its ok to use")                             
  44.      | case Some(x:Deprecated) => println ("it is deprecated dont use!")
  45.      | }
  46. it is deprecated dont use!
  47. scala> if(Some("hi") == Some("hi")) println("a match")    
  48. match
  49. scala> if(Some("hi") == Some("ho")) println("a match")
  50. // After you have tested you can use get to obtain the value
  51. // but becareful, you will get an exception if you forget to test.
  52. scala> res0.get
  53. java.util.NoSuchElementException: None.get
  54.                           at scala.None$.get(Option.scala:169)
  55.                           at scala.None$.get(Option.scala:167)
  56.                           at .< init>(< console>:12)
  57.                           at .< clinit>(< console>)
  58.                           [snip]
  59. scala> res2.get
  60. res10: java.lang.annotation.Annotation = @java.lang.Deprecated()
  61. // a potentially better way of optaining the value is to provide a default if 
  62. // the value does not exists
  63. scala> res2.getOrElse(classOf[java.lang.annotation.Documented])
  64. res13: java.lang.Object = @java.lang.Deprecated()
  65. scala> res0.getOrElse(classOf[java.lang.annotation.Documented])
  66. res14: java.lang.Object = interface java.lang.annotation.Documented
  67. // A Option is a "monad" (dont worry too much about the term if 
  68. // you dont know it) a very (VERY) simplified meaning of that is that 
  69. // option behaves a bit like a collection of size 1 or 0
  70. // you can use similar methods on an Option as a collection.  
  71. // So exists tests each element in the Option to see if it matches 
  72. // the function passed in as the parameter
  73. // res2 has an object that is an instanceOf Annotation 
  74. scala> res2.exists {_.isInstanceOf[Annotation]}  
  75. res7: Boolean = true
  76. // None always returns false to exists
  77. scala> res0.exists {_.isInstanceOf[Annotation]}
  78. res8: Boolean = false
  79. scala> res2.exists {_.toString == "hi"}
  80. res29: Boolean = false
  81. // Similarly you can use the filter method that is present in all collections
  82. scala> res2.filter {_.toString == "hi"}    
  83. res30: Option[java.lang.annotation.Annotation] = None
  84. scala> res2.filter {_.isInstanceOf[Annotation]}
  85. res32: Option[java.lang.annotation.Annotation] = Some(@java.lang.Deprecated())
  86. // apart from just filtering you can convert the type contained 
  87. // in the Option by using map to map from one type of Option to
  88. // another type of Option in this examples we map from 
  89. // Option[Annotation] to Option[String]
  90. scala> res0.map {_.toString}.getOrElse("does not exist")                        
  91. res15: java.lang.String = does not exist
  92. scala> res2.map {_.toString}.getOrElse("does not exist")
  93. res16: java.lang.String = @java.lang.Deprecated()
  94. // Another very handy and safe way to access the value is to use foreach
  95. // this will call the function with the parameter from the Option if
  96. // the Option is Some but a noop will occur if the Option is None.
  97. scala> res2.foreach { println _ }
  98. @java.lang.Deprecated()
  99. scala> res0.foreach { println _ }
  100. // orElse is simply a method to ensure that the Option is a Some
  101. scala> res0.orElse(Some(classOf[java.lang.annotation.Documented]))
  102. res22: Option[java.lang.Object] = Some(interface java.lang.annotation.Documented)
  103. scala> res2.orElse(Some(classOf[java.lang.annotation.Documented]))
  104. res23: Option[java.lang.Object] = Some(@java.lang.Deprecated())

No comments:

Post a Comment