Created
October 23, 2018 10:59
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/********************************************************************************************** | |
* 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