-
much of what you already know about programming is useful in scala
- favour composition over inheritance
- single responsibility principle
- DRY when it makes sense
- don't leak abstractions
- program to an interface
- inversion of control
- principle of least power
-
sometimes these look like their OO counterparts
-
sometimes they are functional, but if you squint at them, they are doing the same thing
- General concensus is that the cake pattern was a bad idea
- It's an overly complicated way to do Dependency Injection
- Using Javax or Guice or whatever is an improvement
- Look into using Macwire (scala specific) if you really want to do DI
- Personally not a fan of dependency injection frameworks
- rarely solves a problem I have
- small services, manual DI is easy and goes a very far way
- DI frameworks get in the way of some functional refactors I try to do
- If you are using DI just to save a few key strokes, implicit parameters can get you pretty far
- this is CS 101
- scala collections are Immutable by default
- know when to use what collection and why
- understand the performance implications of the collection you are using
- if you are indexing into something, don't use List
- building up collections can be performance bottlenecks if you do it in a
stupid way
- e.g. folding an immutable map to add key value pairs makes a new map for every key value (slow)
- better to prepend to a list of (key, value) tuples (constant time) and then .toMap at the end
- in extreme cases, use a Mapbuilder
- become very familiar with "working with values in a context" as this is fundamental in scala / FP
- you want to know map, filter, flatMap, foldLeft, foldRight, fold, exists, any, colllect intimately
- Know what folding an option does (as well as .getOrElse)
- It's important to understand what is going under the hood in a for comprehension. They are NOT for loops. See resources: essential scala for a primer on this.
- just because you can open closures everywhere and inline tons of anonymous functions doesn't mean this is a good idea
- use intermediate variables and give things names when doing something complicated
- keep the right side of for comprehensions simple. Maybe do one operation like a simple map on the right side.
- if you find yourseful doing lots of multi-line logic inside an map or a flatmap or a for comprehension, consider moving it to a function with a good name a docstring to explain what is going on
- use guards in pattern matches sparingly
- avoid complicated pattern matching esp if every line has a guard with a lot of
boolean logic
- this probably means you have a data model problem staring you in the face
- if you have a lot of boolean logic (if && B && C || D) considering moving this to a named function with a docstring
- having a function return multiple things is great
- BUT if you find yourself doing a lot of
_._1 and _._2
to unpack your tuples you are doing it wrong - the runtime cost (memory, etc.) of a tuple and a case class are exactly the same so if it makes sense to give it a name (see previous point) then do so
- local functions return typles that are destructed inline are fine
val A, B = Fn that returns (A, B)
- learn the syntax to inplace destructure tuples when mapping, folding, filtering, etc.
val x = List((1, "thing"), (2, "otherThing"))
x.map {
case (id, description) => ...
}
// ^-- is MUCh better than
x.map(blah => do something with blah._1 , do something with blah._2)
- throw exceptions for the exceptional but don't lie with your type signatures
- if you just need to return something that can fail, consider an Option
- if you care about why something failed, consider Either
- if you care about why a chain of operations failed and you want to know all the things that went wrong, consider Validation
- see learning about the common methods of the collections library.
- there are legitmate use cases for matching on these sometimes, but they are rare, make sure it's not a smell.
- I make lots of ADTs when coding (sealed traits with case classes/objects that extend the trait)
- Make lots of data. Make lots of domain models. Give things good names.
- If Nothing is Something, make it a case class and give it a name
- If the empty list actually means something, make it a case class and give it a name
- Doing a bunch of work and then somewhere far away there is a if
(list.isEmpty){ }
the empty list means something then and consider giving it a name - If you have a case class that takes in several Options it may be a smell that
your model is wrong
- this can happen slowly over time when people start jamming optional parameters into type signatures, then the next person comes along and jams another optional paramaeter in,.. usually a sign there is some underlying refactoring that needs to go on
- you probably want two different threadpools for your blocking and nonblocking operations
- scala has a complicated (but extremely powerful) type system so use it to your advantage
- parametric polymorphism is great! When in doubt, add a type parameter has served me well for code re-use
- more advanced type trickery such as Phantom Types are good tools to have in your toolbox
- the goal of much of FP is to turn as much stuff into compiler errors at compile time rather than exceptions at runtime
- the compiler is your friend and is very smart, let it do the heavy lifting for you
When designing your architecture in the small, understand the difference between expanding the range and restricting the domain
take for example a (contrived) example where we have some function that only works for non empty list
- expand the range: allow the fn to return None for the empty list case
- restrict the domain: push this back on the caller and have th fn only work on
a
NonEmptyList
The first one pushes the responsibility onto the function, the second pushes the responsibility onto the caller. You want to favour the second as much as is ergonomically possible in your code base.
- implicits are useful and it's important understand the basics
Basics:
- passing an implicit parameter
- using implicits for
extension methods
which lets you add method to a class you don't own - basic type classes / type class constraints (see Resources: essential scala has a good chapter on this)
- subtyping is often a smell (ADTs + pattern matching is fine)
- be very wary of casting (
.asInstanceOf
) it usually means you have a domain model problem (but can be useful in tests) - Using a view bound can be occasionally useful but use them sparingly, it usually means you have a domain model problem
- e.g.
def blah[A <: SomeThing]
taking a generic function blah, and restricting it such that the generic parameter A is a subtype of some type. It is occasionally useful, but often hides problems if abused.
- e.g.
- don't use reflection
- use a
Try
to wrap java libraries that may throw (this is dfferent thentry {} catch {}
- Future/Task etc. have
recover
andrecoverWith
for handling the failed future or task that may contain a failure EitherT
has similar things.EIther[E, A]
has aleftMap
when you want to convert an error of one type into an error of another (very common to convert a Throwable into some sort of domain model error instead)- lots of examples in our more modern services
- performance concerns of scala are overblown
- Understand your hot path, tight loops, etc. and make sure you aren't doing something inefficient here
- I don't bother doing performance optimization until someoneone has an actual performanc test, or has hooked up a profiler
- local mutation is fine!
- Sometimes the answer in scala is a while loop and an array, or a while loop and a listBuffer, or a MultiMap, or using some highly optimized third party java collection to solve some performance pain point
- BUT generally avoid leaking mutable objects ouside of local functions.
- turn that listBuffer into a list
- turn that Array into a list
- trick the type system to thinking that a mutable map is actually immutable even though it isn't
- in a tight loop, this can be considerably more performance than say ... a fold, if the collection size is very, very large.
- must have import
- great documentation
- so many useful functions
- Knowing enough to use something like
EitherT
for the common case where you have a Task[Either[Throwable, Result]] as this is (almost) the same as EitherT[Task, Throwable, Result] except much more pleasant to work with
- Task over Future
- better performance
- lazy
- cancellable
- friendler error handling/composition
- great docs
- can wrap Future based libraries
- also has a decent streaming lib
- use
sttp
, decent docs and great synergy with cats/json parsing library
- best library there is for this in scala IMHO
- options are akka-streaming, monix observables, fs2
- everyone loves fs2 (haskell folks at a conference were wishing they had something that nice in haskell)
- the software mill guys blog tons about great scala info, in particular streaming (e..g here). They are a reputable source of information and the authros of the
sttp
library. They have many, many articles about akka-streams, monix, fs2, etc.
- moot? since we do that weird GRPC thing now
- play is fine in terms of experience in hootsuite for it
- I would consider http4s but it's quite on the functional side of things
- use Doobie
In this order:
- Essential Scala
- the first 3-4 chapters of Functional And Reactive Domain Modeling
- Scala With Cats
- fp foundation course is a great resource once you have the basic syntax down and understand a little about map/fold/etc.
- how to build a functional API by the guy who made the Fp foundation course
- constraints liberate, liberties constraint - up the 36 minute mark, after that it's not useful to the beginner
More of an advanced talk, but a very useful mindset to get into eventually:
- programming with effects up to about the 27th minute mark
Another advanced talk that is fascinating in terms of the functional mindset, but much more functional than we do here
-
These repos are QUITE functional (more-so than anything we have running around at work) but are worth looking at.
-
Notice how contained everything is.
- for comprehensions are small/simple/easy to read
- no huge nesting of if statements, pattern matches, anonymous functions, etc.
-
I think they are good examples of what to shoot for in your code in terms of class size/responsibility, function size, for comprehensions