Skip to content

Instantly share code, notes, and snippets.

@JonNorman
Last active November 12, 2018 15:33
Show Gist options
  • Save JonNorman/8b1167f14eafa47ed47ab2c833e8b941 to your computer and use it in GitHub Desktop.
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.
/**
* 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