Skip to content

Instantly share code, notes, and snippets.

@thinkharderdev
Last active November 6, 2018 13:21
Show Gist options
  • Save thinkharderdev/3bf2b2d5162bec12b97837ab0a8184dc to your computer and use it in GitHub Desktop.
Save thinkharderdev/3bf2b2d5162bec12b97837ab0a8184dc to your computer and use it in GitHub Desktop.
Sharpen The Saw #scala
sealed trait UserRole
case object BasicUser extends UserRole
case object Admin extends UserRole
// I can very easily define basic data types using case classes
case class User(id: String, name: String, email: String, role: UserRole)
//Defining User as a case class has a couple of implications
// You don't need to use the "new" key word when instantiating an instance
val user = User("id","Dan","foo@bar.com", Admin)
// What this is actually doing under the hood is calling the apply method on the User companion object.
// So the above expression is desugared to
val user = User.appl("id","foo@bar.com",Admin)
// In this case, the scala compiler is auto-generating the User companion object and apply method
// The scala compiler will auto-generate a bunch of useful stuff for case classes. Such as...
// A copy constructor for creating shallow copies of my object
def makeAdmin(user: User): User = user.copy(role = Admin) // here we can just copy the original object with a new role
// The "scaffolding required for pattern matching
def isAdmin(user: User): Boolean = user match {
case User(_,_,Admin) => true
case _ => false
}
// An object is just a singleton, but one that is enforced by the runtime.
// It can have internal state but it is only initialized once
// A "companion" object is just an object that is a "companion" (that is, has the same name) as a class
class MyClass(state: String)
// Here is MyClass's "companion object"
object MyClass {
// In this case we just have a static constant. In the java world this would be a static variable on MyClass itself
// In Scala, by covention anything that would be declared static in Java just goes in the companion object
val SOME_STATIC_CONSTANT: String = "foo"
}
// But, there is also some syntactic sugar associated with objects
// For example, the apply method can be called using just parens. So
object MyObj {
def apply(s: String): String = ???
}
// We can call MyObj.apply like so
MyObj("foo")
// Which gets desugared to
MyObj.apply("foo")
// As noted before, this is what is happening with case classes. The compiler is auto-generating the companion object and apply method,
// which is why we don't need to use the new keyword
// Methods in scala can be defined with multiple separate argument lists. For example
// from the Traversable trait we have an example
// This method starts with a base value z and then traverses the collection applying the pairwise operation op
// to the output of the previous iteration and the next element
def foldLeft[B](z: B)(op: (B, A) => B): B
// So, I if I wanted to sum a list on Ints I could do it like so
val ints = List(1,2,3,4)
ints.foldLeft(0)((sum,next) => sum + next) // result is 10
// This is for a more concise syntax (IMHO). But also allows for easy currying.
// For example we could do something like this
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val numberFunc = numbers.foldLeft(Lis.empty[Int])
val squares = numberFunc((xs, x) => xs:+ x*x)
print(squares.toString()) // List(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)
val cubes = numberFunc((xs, x) => xs:+ x*x*x)
print(cubes.toString()) // List(1, 8, 27, 64, 125, 216, 343, 512, 729, 1000)
// An expression can a normal assignment statement like we see in other languages
val e1 = "foo"
// It can also be a "block" which is a series of simple expressions which takes the type of the last simple expression to execute
val e2: String = {
val inner = "foo"
if (inner.startsWith("f")) "goo" else "foo"
}
def myMethod(arg: String): String = {
s"$arg-someextrastuff"
}
// We can call our method the "normal" way
myMethod(e2)
// But we can also pass a complex expression as an argument to a method
myMethod {
val inner = "foo"
if (inner.startsWith("f")) "goo" else "foo"
}
// This really makes immutability less onerous because we can just initialize immutable vals to the return value of a block
// So instead of writing
val user: User = ... //
var username = null;
if (user.isBusinessUser) {
username = user.businessName
} else {
username = user.username
}
//We can write
val username = if (user.isBusinessUser) user.businessName else user.username
// Pattern matching is kina of like case/switch on steroids.
// Using plain "vanilla" pattern matching we can do a number of things very nice and concisely
// Example copied shamelessly from https://docs.scala-lang.org/tour/pattern-matching.html
abstract class Notification
case class Email(sender: String, title: String, body: String) extends Notification
case class SMS(caller: String, message: String) extends Notification
case class VoiceRecording(contactName: String, link: String) extends Notification
// We can do basic pattern matching on case classes
def showNotification(notification: Notification): String = {
notification match {
case Email(email, title, _) =>
s"You got an email from $email with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
case VoiceRecording(name, link) =>
s"you received a Voice Recording from $name! Click the link to hear it: $link"
}
}
val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
println(showNotification(someSms)) // prints You got an SMS from 12345! Message: Are you there?
println(showNotification(someVoiceRecording)) // you received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123
// We can also use "guards" to condition the match
def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String = {
notification match {
case Email(email, _, _) if importantPeopleInfo.contains(email) =>
"You got an email from special someone!"
case SMS(number, _) if importantPeopleInfo.contains(number) =>
"You got an SMS from special someone!"
case other =>
showNotification(other) // nothing special, delegate to our original showNotification function
}
}
val importantPeopleInfo = Seq("867-5309", "jenny@gmail.com")
val someSms = SMS("867-5309", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
val importantEmail = Email("jenny@gmail.com", "Drinks tonight?", "I'm free after 5!")
val importantSms = SMS("867-5309", "I'm here! Where are you?")
println(showImportantNotification(someSms, importantPeopleInfo))
println(showImportantNotification(someVoiceRecording, importantPeopleInfo))
println(showImportantNotification(importantEmail, importantPeopleInfo))
println(showImportantNotification(importantSms, importantPeopleInfo))
// Or we can match on type only. This is like doing instanceof in Java
abstract class Device
case class Phone(model: String) extends Device{
def screenOff = "Turning screen off"
}
case class Computer(model: String) extends Device {
def screenSaverOn = "Turning screen saver on..."
}
def goIdle(device: Device) = device match {
case p: Phone => p.screenOff
case c: Computer => c.screenSaverOn
}
// In a way, pattern matching is just syntactic sugar for Extractor objects. I won't cover that here for time
// but checkout https://docs.scala-lang.org/tour/extractor-objects.html for more info obout how to "customize"
// pattern matches.
import scalaz._
import Scalaz._
def sum(ns: List[Int]): Int = ns.foldRight(0)((sum,next) => sum + next)
def all(bs: List[Boolean]): Boolean = bs.foldRight(true)((sum,next) => sum && next)
def concat[A](ss: List[List[A]]): List[A] = ss.foldRight(List.empty[A])((sum,next) => sum ::: next)
trait Combiner[A] {
def combine(l: A, r: A): A
def zero: A
}
def genericSum[A](as: List[A])(implicit c: Combiner[A]): A = {
as.foldRight(c.zero)((sum,next) => c.combine(sum,next))
}
implicit val intCombiner = new Combiner[Int] {
override def combine(l: Int, r: Int) = l + r
override def zero = 0
}
implicit val boolCombiner = new Combiner[Boolean] {
override def combine(l: Boolean, r: Boolean) = l && r
override def zero = true
}
genericSum(List(1,2,3),intCombiner)
def genericerSum[M[_],A](as: M[A])(implicit c: Combiner[A], folder: Foldable[M]): A = {
folder.foldRight(as, c.zero)((sum,next) => c.combine(sum,next))
}
implicit def pairCombiner[A,B](implicit aCombiner: Combiner[A], bCombiner: Combiner[B]): Combiner[(A,B)] = new Combiner[(A,B)] {
override def combine(l: (A, B), r: (A, B)) = (aCombiner.combine(l._1, r._1),bCombiner.combine(l._2,r._2))
override def zero = (aCombiner.zero,bCombiner.zero)
}
genericerSum(List(1,2,3))
genericerSum(Set(1,2,3))
genericerSum(List((1,true),(2,true),(3,false)))
//Scalac will infer that foo is of type String
val foo = "foo"
//But you can also make the type explicit
val foo: String = "foo"
//Scalac will also infer type arguments
def myFunc[S](arg: S) = ??? //Here myFunc has a type parameter S which defines the type of it's argument arg
//When invoking myFunc, scalac will infer the type argument by the value passed to it
myFunc("foo") //Scalac infers that the type argument is String since arg is of type String
val l = List(1,3,3)
case class MyContainer[T](maybeValue: Option[T])
case class MyOtherContainer[T](value: T)
//Here's a contrived example where we let the compiler infer all type arguments
l.map(n => MyContainer(Some(n)))
.collect {
case MyContainer(Some(v)) if v < 3 => MyOtherContainer(v)
}
//Now the same example where we explicitly provide all type arguments
l.map[MyContainer[Int],List[MyContainer[Int]]](n => MyContainer(Some(n)))
.collect[MyOtherContainer[Int],List[MyOtherContainer[Int]]] {
case MyContainer(Some(v)) if v < 3 => MyOtherContainer(v)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment