-
-
Notifications
You must be signed in to change notification settings - Fork 5
Monocly contributor documentation
Welcome to Monocly, the R&D lab for Monocle 3. We grow experiments here, adding them back into the Monocle 3.x branch as they mature, or deleting them and replacing them with confirmed 3.x code as we go. This codebase will not "become" Monocle 3; it is a source of ideas & code to be copied back into the main repo.
Focus is the flagship feature of Monocle 3, currently being developed here in Monocly. It is a new macro that handles 80% of the common macro use cases requiring no particular knowledge of optics, using a conventional approach that would be familiar to users of XPath or jq.
Focus[Company](_.employees.each.role.bonus.some) // Generates a Traversal[Company, Money]
bobsBurgers.focus(_.employees.each.role.bonus.some).getAll() // Applies the generated traversal to bobsBurgers, returning a List[Money]
We welcome contributors, it's a great way to cut your teeth on Scala 3 metaprogramming. Look out for the issues marked as "Good first issue"!
The front door to the Focus macro lives at optics.poly.Focus. The implementation is all marked package-private, and stashed away in optics.internal.focus.
Scala 3 macros, for most writers, consist of nested quotes ('{...} for terms, '[...] for types) and splices ${...} which dynamically assemble code inside the compiler, in a way that would be fairly familiar to macro users in other languages like Lisp. You can learn more about them here.
Because this macro is performing some quite advanced calculations, we need to drop down to the next level down from Exprs and Types; that is, Terms and TypeReprs. These types are only available inside the reflect field of the macro implementation's implicit scala.quoted.Quotes context. The reflect object gives access to the internals of TASTy (Typed Abstract Syntax Trees, Scala's pre-bytecode intermediate language that allows for compatibility and translation between Scala versions). It is quite possible to generate uncompilable nonsense through this interface, so care must be taken.
The best source of documentation for Quotes is the code itself. It is essentially an interface declaration that does not betray any implementation, but describes all the constructs expressible in the Scala language. All the types are bare type declarations, like type Term; "static" methods like Term.blah are found on TermModule and extension methods on Term instances are found in TermMethods. All of the concepts use this naming convention. You can quickly skip to anything by typing (concept)M (eg TermM, SelectM, etc) in your browser search bar, which will immediately bring you to the XXXModule and XXXMethods definitions. I find it useful to have a link to the Quotes code in my browser bookmark toolbar, which lets me quickly skip in and out of the doco without breaking flow.
The macro flow is broken into two phases:
-
Parsing: Parse the supplied code into a list of
FocusActions, describing what is to be done. -
Generation: Generate code that creates an optic for each
FocusActionand composes them together.
The FocusImpl class shows the two phases being run, and either returning the generated code or reporting an error.
The Focus codebase uses the so-called "cake pattern", meaning that functions live in traits, and dependencies between the traits are expressed as self-types, that is class MyModule {this: SomeOtherModule => ...}. This pattern works nicely for this style of code, but is also necessary, so that the compiler can sew together all the types that hang off the one instance of Quotes and treat them the same way.
The FocusBase module is used by every module in the macro. It provides:
- The macro's
scala.quoted.Quotesinstance, calledmacroContextto avoid conflicting withscala.quoted.quotes. - The
FocusActionenum, with a case per "thing" that can be described with the macro argument. - The
FocusErrorenum, which describes every possible error condition obtainable inside the macro. type FocusResult[A] = Either[FocusResult, A]-
trait FocusParser, which describes the slightly complicated type signature that Parser modules need to satisfy.