Skip to content

Instantly share code, notes, and snippets.

@JonNorman
Created October 23, 2018 10:59
Show Gist options
  • Save JonNorman/715ce5c2ca17a1ccdcc9bbb1592b2614 to your computer and use it in GitHub Desktop.
Save JonNorman/715ce5c2ca17a1ccdcc9bbb1592b2614 to your computer and use it in GitHub Desktop.
A quick overview of the concepts covered in week 4 of scala-school (part 1)
/**********************************************************************************************
* Case Classes
*
* Creating our own types (through classes and, later, traits) allows us to better model the
* domain we are working in. Create higher level concepts and manipulate them more intuitively
**********************************************************************************************/
/*
Let's say we want to calculate the distance between two cartesian points on a regular X-Y graph.
We could do it like so (using pythagoras's theorum of length = (x^2 + y^2) ^ 1/2):
*/
def calculateDistance(x1: Int, y1: Int, x2: Int, y2: Int): Double = {
val xDistance = Math.abs(x1 - x2)
val yDistance = Math.abs(y1 - y2)
Math.sqrt(xDistance * xDistance + yDistance * yDistance)
}
calculateDistance(0, 0, 4, 0)
calculateDistance(0, 0, 4, 3)
/*
Some extra things you might see above that you're not used to yet:
The Math.abs is a function that is included in the scala standard library, this is a library
of functions that are included by default every time you create a new Scala program.
The 'Maths' part indicates that it is part of the 'Maths' package and, within this
package the 'abs' function is defined. We use namespaces so that we can import two
libraries that might declare functions with the same name e.g. Maths.abs and Gym.abs - as
long as the namespaces are different, we can distinguish between them.
*/
/*
That function works, but at first glance, it isn't clear what it is doing or why - what is x1 and x2?
Are they related? Or does x1 go conceptually with y1?
Instead of passing around single pieces of data like this, we can encapsulate them in something more
meaningful, and we can do this by creating our own type with case classes.
*/
// our new type contains two fields, an x and a y, both are Ints
case class Coordinate(x: Int, y: Int)
// we can create an instance of this class by doing the following:
val myPoint = Coordinate(4, 2)
val theOrigin = Coordinate(x=0, y=0)
// by passing in the values for x and y, scala can create a new instance with those values inside of it
// as with method definitions, you can pass in arguments by position or by name with 'name=value' syntax.
// Now let's try rewriting our function about and see how it looks
def calculateDistance2(a: Coordinate, b: Coordinate): Double = {
val xDistance = Math.abs(a.x - b.x)
val yDistance = Math.abs(a.y - b.y)
Math.sqrt(xDistance * xDistance + yDistance * yDistance)
}
/*
When defining a case class you can add other values that are derived from its inputs. Because
we know that x and y can't change - they are immutable - once the class has been instantiated,
we can set it as a value on the case class itself like so:
*/
case class Coordinate2(x: Int, y: Int) {
val distanceFromOrigin = Math.sqrt(Math.pow(Math.abs(x), 2) + Math.pow(Math.abs(y), 2))
}
val myNewPoint = Coordinate2(4, 3)
myNewPoint.distanceFromOrigin
/*
As with functions, we can add default arguments. In this example, our Coordinate can represent
a point in 3D space, but we might generally want to just keep things 2D and so provide a default
value of 0 for the z argument.
*/
case class Coordinate3(x: Int, y: Int, z: Int = 0)
/*
One thing you will often see with case classes is that they have 'companion objects' i.e. an
object defined with the same name as the case class that has a bunch of useful functions in it.
These are defined with the word 'object' and the name of the object.
An object is sometimes referred to as a 'Singleton class' - all this means is that when your program
is loaded and initialised, the object will be initialised once and there can be no more instances of
that object created. So whereas we can create as many Coordinate3 instances as we like, there is only ever
a single Coordinate3 object and all of the useful functions and values therein will only be
created once.
*/
object Coordinate3 {
def distanceBetween(a: Coordinate3, b: Coordinate3): Double = {
Math.sqrt(
Math.pow(Math.abs(a.x - b.x), 2)
+ Math.pow(Math.abs(a.y - b.y), 2)
+ Math.pow(Math.abs(a.z - b.z), 2)
)
}
}
Coordinate3.distanceBetween(Coordinate3(5,8), Coordinate3(6,7,9))
/*
Note: what follows might sound a bit complex, but it isn't once the simple concepts are understood.
if this doesn't make sense then come and ask! It might be easier to explain in person...
One other useful thing that you can do with companion objects is that you can create alternative
constructors for your case class. A 'constructor' is a function that, when called, will return
an instance of your class. Whenever you define a new case class, scala will create a default constructor
for you "under the hood" in the case class's companion object.
Constructor functions are always called 'apply' followed by their arguments. You can explicitly call
Coordinate.apply(1,6) but if you just do Coordinate(1, 6) then the compiler will know what you mean
and will look for an apply method within that object that fits that signature.
For the classes we defined above these would look like:
*/
// ignore this outer object 'a' - it is just so that we can define 'object Coordinate3' again. We've
// already defined it, above, and objects can't be declared twice in the same namespace.
object a {
object Coordinate {
def apply(x: Int, y: Int): Coordinate = {
new Coordinate(x, y)
}
}
object Coordinate3 {
// the default constructor
def apply(x: Int, y: Int, z: Int): Coordinate3 = {
new Coordinate3(x, y, z)
}
// the constructor that allows us to have a default of 0 for our 'z'
def apply(x: Int, y: Int): Coordinate3 = {
new Coordinate3(x, y, 0)
}
}
}
/*
If we don't explicitly create the companion object for our class then scala will do it for us
and will create the default 'apply' methods under the hood. If we DO decide to create the
companion object ourselves then we don't need to write the default apply methods either, scala
will still just add them to that object, but we can define other constructor methods if we want to.
*/
case class Rectangle(height: Int, width: Int)
object Rectangle {
// maybe we want to add a constructor to create a special kind of Rectangle, a square
def apply(heightAndWidth: Int): Rectangle = {
Rectangle(heightAndWidth, heightAndWidth)
}
// maybe we want to add a constructor to create a unit square by referencing our OTHER constructor?
def apply(): Rectangle = {
Rectangle(1)
}
// maybe a constructor that takes the corners as arguments and derivse the dimensions?
def apply(bottomLeft: Coordinate, topRight: Coordinate): Rectangle = {
Rectangle(topRight.y - bottomLeft.y, topRight.x - bottomLeft.y)
}
}
Rectangle(5)
Rectangle(5,6)
Rectangle()
Rectangle(Coordinate(0, 0), Coordinate(5,6))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment