Last active
November 12, 2018 15:33
-
-
Save JonNorman/8b1167f14eafa47ed47ab2c833e8b941 to your computer and use it in GitHub Desktop.
A recap of some of the concepts covered in week6 - predominantly how to read and write function definitions.
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
/** | |
* Functions! | |
* | |
* We've had some practice in writing our own functions, but let's go back to basics and go | |
* through what they are and how we can write them. | |
* | |
* Feel free to skip to the TL;DR at the bottom... | |
* | |
* So, what is a function? | |
* | |
* Simply put A function is an expression that takes an input of one or more types and returns an output of another type. | |
* | |
* Most commonly these are defined using the `def` keyword as below | |
*/ | |
def checkStrings(a: String, b: String): Boolean = { | |
val lengthMatches = a.length == b.length | |
val aIsUppercased = a.toUpperCase == a | |
lengthMatches && aIsUppercased | |
} | |
/** | |
* Anatomy of a Function: | |
* | |
* The "name" of the function is the label we provide after the `def` keyword i.e. `checkStrings` | |
* | |
* The "arguments" or "parameters" of the function are provided in parentheses after the function name. Each argument | |
* is given a name and a type. i.e. `(a: String, b: String)` | |
* | |
* The return type of the function can be specifed after the arguments list by using a `:` (colon) and then the type. | |
* Scala can usually infer the return type of your function, so you often need not explicitly specify it, but it can | |
* make your code more readable. i.e. `Boolean` | |
* | |
* The above components comprise the function "signature", and this needs to be unique amongst all the other functions | |
* in scope. One notation for specifying the signature of our function is `checkStrings = (String, String) => Boolean`. | |
* | |
* The "body" of the function comes after the function signature, it begins with an `=` (equals) sign and is usually | |
* followed by a block of code enclosed with curly braces. If the function is simple and comprises only a single | |
* expression, then the curly braces need not be used. | |
* | |
* The final statement of your function body is what will be returned by the function when it is called. We call/invoke | |
* our function by using parentheses and supplying the arguments in those parentheses. | |
* | |
* Important: each time we invoke our functions, we cause the body of the function to be executed. | |
*/ | |
checkStrings("test", "TEST2") | |
checkStrings("TEST", "code") | |
/** | |
* Scala provides a rich syntax for defining and using functions, which is one reason that you might read a lot more on | |
* "functional programming" (FP) in scala than other languages. We aren't going to go into functional programing explicitly | |
* here, but the approaches and best practices we will cover will be heavily influenced by FP principles. | |
* | |
* One benefit that we have seen before is that we can write lots of small, simple functions and we can compose them | |
* together to express far more complex behaviour. We have seen this a lot and it is how we shall write our programs. | |
*/ | |
/** | |
* TL;DR There are 4 (main) ways to define a function that you will see and write (in descending order of prevalence): | |
* | |
* - 1. Using `def` i.e. the way you will usually do it | |
* - 2. Anonymous functions (explicit syntax) i.e. with a named argument | |
* - 3. Anonymous functions (shorthand syntax) i.e. user the underscore | |
* - 4. Using `val` i.e. defining a function and assigning it to a `val` | |
* */ | |
// using `def` | |
def add1(a: Int, b: Int): Int = a + b | |
add1(1,3) | |
// using `val` with no explicit type (requires types on the input values) | |
val add2 = (a: Int, b: Int) => a + b | |
add2(1,3) | |
// using `val` with an explicit type (scala will infer the types of the input values) | |
val add3: (Int, Int) => Int = (a, b) => a + b | |
add3(1,3) | |
// anonymous functions (using Users as a made-up model example) | |
import java.time.LocalDate | |
case class ProductSub(product: String, dateSubscribed: LocalDate) | |
case class User(username: String, products: List[ProductSub], isPremium: Boolean) | |
// create a made-up list of guardian Users | |
val users = List( | |
User("amy", List( | |
ProductSub("newspaper", LocalDate.parse("2017-06-01")), | |
ProductSub("app", LocalDate.parse("2018-01-01")) | |
), true), | |
User("jin", List( | |
ProductSub("app", LocalDate.parse("2018-11-01")) | |
), false), | |
User("sara", Nil, false) | |
) | |
// define a function using `def` | |
def isPremiumSubscriber(user: User): Boolean = user.isPremium && user.products.nonEmpty | |
/* using named `def`, note we are passing our function as an argument to the `List.filter` function. | |
We aren't calling our function explicitly, the body of the `List.filter` function will do this and scala | |
is happy for us to pass in `isPremiumSubscriber` into `filter` as it knows the type of the function matches | |
what `filter` requires. | |
*/ | |
val premiumSubscribers = users.filter(isPremiumSubscriber) | |
// supplying a simple anonymous function | |
val premiumSubscribers2 = users.filter(u => u.isPremium && u.products.nonEmpty) | |
// supplying a simple function with shorthand (underscore) operator. | |
// The `_` acts as a placeholder for the arguments in the anonymous function | |
val usersWithMultipleSubscriptions = users.filter(_.products.size > 1) | |
// an anonymous function that takes up more than one expression, we therefore need to use curly braces | |
val usersWithRecentDigitalSubscriptions = users.filter { user => | |
val recencyThreshold = LocalDate.parse("2018-09-01") | |
val digitalSubscriptions: List[ProductSub] = user.products.filter(_.product != "newspaper") | |
val recentDigitalSubscription = digitalSubscriptions.find(_.dateSubscribed.isAfter(recencyThreshold)) | |
recentDigitalSubscription.isDefined | |
} | |
/* | |
Advanced / Obscure: | |
As mentioned above, we can define functions to be stored as `val`s as well but you will rarely see this as it is | |
verbose and annoying to write/express. We cover it just because it will get you used to reading function signatures | |
and you should be aware that functions can be defined in this way. | |
*/ | |
val isPremiumSubscriber2 = (user: User) => user.isPremium && user.products.nonEmpty | |
users.filter(isPremiumSubscriber2) | |
/** | |
* As you can see, there are a lot of ways of defining and invoking functions! | |
* | |
* How should you pick between these ways of expressing a function? | |
* | |
* Some pointers: | |
* | |
* 1. Generally don't use the `val` syntax. It is annoying to use and is limited in important ways that `def` isn't. | |
* | |
* 2. If you want to refer to this function again - perhaps because it is used in your code elsewhere or you want to | |
* test it - then you should use the `def` syntax. | |
* | |
* 3. If your function is going to be used once, is simple, and is being passed as an argument, use the anonymous syntax. | |
* If it's _really_ simple then use the shorthand notation, otherwise if it is more involved it is probably | |
* better to use the explicit notation. | |
*/ | |
/** | |
* Before we go through more in-depth: the following examples use `assert` to check that the results of calling our | |
* functions is what we expect. You may not have seen this before, but this idea of checking our expectations will be | |
* dealt with more fully when look at writing tests via scala test. | |
*/ | |
// the input and output types can be the same | |
def square(a: Int): Int = a * a | |
def exclaim(s: String): String = s + "!!!" | |
assert(square(2) == 4) | |
assert(square(5) == 25) | |
assert(exclaim("Hello") == "Hello!!!") | |
// the input and output types can be different | |
def isAnExclamation(s: String): Boolean = s.endsWith("!!!") | |
def adultStatus(age: Int): String = if (age >= 18) "Senior" else "Junior" | |
isAnExclamation("Hello!!!") | |
isAnExclamation("How are you?") | |
adultStatus(29) | |
adultStatus(10) | |
// there can be several inputs of varying types | |
def calculateRectangleVolume(width: Int, height: Int, depth: Int): Int = width * height * depth | |
assert(calculateRectangleVolume(2,5,7) == 70) | |
assert(calculateRectangleVolume(1,1,1) == 1) | |
/* there can be NO inputs (technically the type for "nothing" or "void" is `Unit`), so this function goes from Unit | |
to String. An input of `Unit` be defined with an empty set of parentheses after the function name or with no | |
parentheses at all. | |
As stated above, each time a function is called, the body of that function is computed, so if there are no inputs | |
to the function, then you might consider using a `val` rather than a function. | |
Question: What kind of cases might you want to still use a function? | |
You can use either syntax but conventionally we use () when the function is doing something effectful - i.e. it has | |
side effects elsewhere like reading in a file from disk - and we drop the parentheses when it is a simple definition: | |
*/ | |
// perhaps this goes an performs some action | |
def fetchCurrentWeatherStatus(): String = "Rainy" | |
assert(fetchCurrentWeatherStatus() == "Rainy") | |
// nothing else is being affected here, so we conventionally drop the parentheses | |
def getMinNaturalNumber: Int = 0 | |
assert(getMinNaturalNumber == 0) | |
// the OUTPUT of a function can also be Unit - again such a function definition would indicate that the function has | |
// some side-effect and it is being called for that purpose | |
def printMessage(message: String): Unit = println("*** MESSAGE: " + message + "***") | |
assert(printMessage("Hi there, welcome to the app") == ()) | |
/** | |
* Conceptually, there is nothing special about functions - unlike other "normal" values that have types like Int and String, | |
* a function expression also has a type, and it can be stored as a value via the `val` syntax we've seen a lot of. | |
* | |
* Below we define the functions we expressed above, again, but this time using the val syntax. | |
*/ | |
val square2 = (a: Int) => a * a | |
assert(square2(2) == 4) | |
assert(square2(5) == 25) | |
/** | |
* This is the equivalent function expression as our `def square` above. | |
* | |
* The compiler needs to know the types of the inputs and outputs to our functions. In a lot of places it can infer | |
* it if we don't specify. | |
* | |
* Below, when defining `square3`, we explicitly specify the type of the function as going from `Int` => `Int`, which | |
* means that in the function body, the compiler can infer that `a` must be an `Int` and we needn't specify. | |
*/ | |
val square3: Int => Int = a => a * a | |
// if we only have one input type then we don't need to use parentheses when specifying it's type | |
val isAnExclamation2: String => Boolean = s => s.endsWith("!!!") | |
assert(isAnExclamation2("Hi!!!")) | |
assert(!isAnExclamation2("Whatever")) | |
// if we don't explicitly specify the type of the function, then we have to give our parameters types, which requires | |
// wrapping our inputs in parentheses | |
val adultStatus2 = (age: Int) => if (age >= 18) "Senior" else "Junior" | |
val calculateRectangleVolume2 = (width: Int, height: Int, depth: Int) => width * height * depth | |
assert(calculateRectangleVolume2(1,1,1) == 1) | |
assert(calculateRectangleVolume2(2,5,7) == 70) | |
// we can use an empty parentheses to specify a Unit (i.e. empty) input, but again, ask whether this should be | |
// a function expression or just a plain old value... | |
val getMinNaturalNumber2: () => Int = () => 0 | |
assert(getMinNaturalNumber2() == 0) | |
val printMessage2: String => Unit = message => println("*** MESSAGE: " + message + "***") | |
printMessage2("Hi there, welcome to the app") | |
/** | |
* This syntax can be a little hard to parse when first introduced and most of the time functions are not written | |
* and saved in vals. It is verbose and repetitive, and it has limits that we will see when we cover "generic types" | |
* later on. | |
* | |
* Note that if we want to call our function, we have to use parentheses even if there are no arguments. This is because | |
* if we just write `val myLowNumber = getMinNaturalNumber2` then `myLowNumber` will just become a copy of the function. | |
* | |
* Look at the types of x and y below | |
*/ | |
val x = getMinNaturalNumber2 | |
val y = getMinNaturalNumber2() | |
/** | |
* Again functions are just values, like Int and String, and they too can be passed around in the same way. | |
* | |
* We've seen previously that we often need to pass functions around to other functions. An example of this has been | |
* used at the start of this file: `List.filter` | |
*/ | |
/** | |
* Advanced: BONUS BIT ON THE UNDERSCORE NOTATION | |
* | |
* The `_` acts as a placeholder for the parameters in an anonymous function | |
*/ | |
List(1,2,3).map(_ * - 1)) | |
// is equivalent to | |
List(1,2,3).foreach(number => number * -1) | |
/** | |
* You will normally only see one underscore in anonymous functions, there are some instances where you may see | |
* multiple, such as below | |
*/ | |
val numbers = List(0, 1,6, 9, 10) | |
numbers.reduceLeft(_ + _) | |
// is equivalent to | |
numbers.reduceLeft((a, b) => a + b) | |
/** | |
* Here, each successive `_` refers to the next argument in the input, here `reduceLeft` expects a function of the | |
* type (Int, Int) => A (where `A` is just the return type of our anonymous function, in this case, also `Int`.) | |
* | |
* So by providing a function `_ + _`, scala replaces the first `_` with the first parameter being passed in by | |
* `reduceLeft` and the second `_` with the second parameter being passed in. | |
* | |
* Unless the functions are very simple, as above, it is generally better to use explicit names for variables | |
* rather than lots of underscores. | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment