Hopefully the previous exercises demonstrated that there are downsides and costs to direct coupling against external APIs like a filesystem.
One of the classic ways to decouple from an external API is to abstract it into an interface implemented by the API. In "production" the real API is used while during testing a mock version is substituted.
This version of the scanner uses this strategy, creating a Filesystem trait that defines the operations
the scanner needs from a real filesystem:
- classifying a file path as a file or a directory
- listing directory contents
- querying the length of files
The scanner then needs to be parameterized on a Filesystem implementation, which requires passing it to all the code sites where
filesystem operations are used. The Reader monad is an elegant way to inject dependencies into
code and well supported by
Eff framework.
In fact, the scanner depends upon two external dependencies; not the filesystem but also topN, the number of largest files to report on. So two distinct reader "effects" are combined.
We'll need to modify the Task-based version of the program to also support these new Reader effect. Eff lets us stack multiple effects in a single monad type.
This version introduces the Eff framework.
Examine the changes to PathScan.scan and identify:
-
The return type is now an Eff expression
Eff[R, PathScan]. This is read as a program that when run has effects described by the effect setRand yields aPathScanresult. -
The effect set
Rpassed a type parameter. -
The member typeclasses
_filesystem,_configand_taskthat denote the effects thatRmust include for this function to operate (Rcan also include other effects not used in this function). -
The
Tasktype used in the previous task exercise has been replaced by a task effect, which can be combined with reader effects despite the two have different behaviors. The_taskmember tells us that the task effect is present. -
The
for {..} yieldexpression in the function body. Eff programs are built by flatMapping over a sequence of sub-steps. Note theaskstep that yieldsfs(the currentFilesystem). Where does theaskmethod come from?
Examine main. Note how we build the Eff program first, then interpret (run) it. To run the program, both
Reader effects need their dependency provided. Once these effects are resolved, the call to runAsync "gets rid" of the
Eff wrapper and leaves us with a Task, which we can run using runSyncUnsafe(1.minute) just as in the task exercise..
- What happens if you re-order the two calls to
runReader? - What happens if you remove one call to
runReader? - Can you reorder the call to
runAsyncrelative to therunReadercalls?
Extend the use of the Reader effect to takeTopN and FileSize.ofFile. Both of these methods should be converted
to:
-
Accept a type parameter
Rand one of the member typeclasses (_filesystemand_config) denoting the dependency they need. -
Return their result in an Eff expression
-
Use a for-expression internally to
askfor their dependency, and thenyieldtheir result.If you've made the changes correctly, there shouldn't be any manual passing of the
FilesystemorScanConfigparameters.
Run the test to verify your changes are working correctly
Note how the ScannerSpec tests most of the program's logic using Plain Old Scala Objects and without doing IO
Examine the DefaultFilesystem.listFiles method and note the try/finally construct. The reason for this is
that listFiles returns a Stream of the directory contents, which holds open a file handle until the stream is cleaned up.