In this exercise we'll look at how we can transform within effect "families" using functions and Monocle optics.
We have seen several examples of effects which have a second type parameter in addition to the payload:
Either[E, ?]has the type of the errorEReader[A, ?]has the type of value to be provided/injectedAState[S, ?]has the type of the stateS
We can think of these effects as being families of related effects, sharing the same basic mechanism but with a particular
type focus. But, eg Reader[A, ?] and Reader[B, ?], are different effects in the Eff effect stack and can't be combined.
The recommended approach is where possible, to work in terms of one effect from an effect family across your program
(ie one Reader, one State). This tends to be the most robust and reliable solution from a type inference perspective.
However, it's sometimes necessary or convenient to be able to combine effects from the same family, but with different foci,
together. You may need to integrate modules written by different authors that both focus on types from their own modules,
for example. So we'll look at how you can do that:
-
To transform a
Either[E, ?]intoEither[E1, ?], we simply need a functionE => E1and we canleftMapit (Eitheris a covariant functor).Writerworks similarly. -
To transform a
Reader[A, ?]intoReader[B, ?], we need a functionB => Ato transform the input while we read it (Readeris a contravariant functor). -
State[S, ?]consumes and emits itsSvalue, so a single function won't do. We need a bidirectional transform betweenSandTto produce aState[T, ?]effect. This is what aLens[S, T]is.
In this exercise, we'll look at transforming two Reader effects to a common shared type, but the approach should extend
to any effect that has a focus type.
-
You should have a conceptual idea of how Monocle lenses work to follow this exercise.
-
Recall in previous examples we had two distinct readers for two different type of configuration, the
Filesystemand the number,topN, of the largest files that the scanner would keep track of. Now, there is a new classAppConfigthat models the applications config, which has these two config items as fields. We want to write the overall program purely in terms ofAppConfigand transform itsReadereffect into readers of the other two subtypes.Find
AppConfigand its companion. Note that it has an implicitLensdeclared betweenAppConfigandFilesystem. -
Note that
pathScanhas a single reader effect_appconfig, but calls methods liketakeTopNandFileSize.ofFilethat declare different reader effects_configand_filesystemrespectively. So effect transformation is occurring here implicitly, driven by the type system. -
The mechanism used to transform effects is
EffOptics.readerLens. We wants totransformthe membership typeclasses, yielding a typeclass that certifies membership for the transformed focus. The~>denotes a natural transformation, which is a "higher kinded function" from anA[_]to aB[_].Note that
EffOptics.readerLensis marked implicit, so it is wired in by the compiler whenever a method is searching for an effect member typeclass. However,EffOptics.readerLensitself trails an implicit dependency upon aLens[S, T]to be in implicit scope.
-
Complete the implementation of the transform
applymethod inEffOptics.readerLens; ie how can you use aLens[S, T]to build aReader[T]from aReader[S, ?]? -
Add a second lens in
AppConfigtoScanConfig, using Monocle'sGenLensmacro. Ensure it's markedimplicitso it can passed by the compiler when effects need to be transformed.
Run the unit tests to check that your implementation is correct.
-
Why did we use
Lensand not justA => Bto transform ourReadereffects? A function would have worked, as the bidrectional nature ofLensis only required forStateeffects where the change of focus is two-way.The reason we avoided function was because we didn't want to depend upon any function
A => Bimplicitly. It's a very generic type and likely to lead to ambiguity. Lens is a less common type so we could be more confident that there wouldn't be random lenses already in implicit scope.