As a long-time Haskeller writing Rust in production, I always miss the powerful Template Haskell macros - Rust’s macro system is rather ad-hoc and weaker than Haskell (it fails to handle hygienity properly, and, crucially, lacks reification mechanism at all!).
But there has always been one thing that I miss one Rust macro mechanism in Haskell: attribute macros.
They are so powerful when combined with their (macro-based) deriving mechanism, as one can use it to control the deriving strategy in the granularity of type/data constructor or fields.
For example, the following derives Deserialize instance from serde (something like aeson or autodocodec in Rust):
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Outer {
id: String,
#[serde(rename = "maxValue"))]
max_value: u64,
#[serde(default = "LogLevel::Info")]
log_level: LogLevel
}
Recently, I heard about Modifiers extension will be shipped with GHC 10, and I realise that we are can achieve the closer thing with it!
…And, here it is:
In short, Modifiers extension allows several places of source code to be attributed with types, and it is a generalisation of a %1 -> b syntax introduced in LinearTypes to allow arbitrary kinds to appear inside %(..).
In this example, Types appearing in field attribute to be used as an implementation selector when deriving instances.
(One caveat: for simplicity, we are just using plain type to annotate it representation - but it can easily collide with other library / compiler features using modifiers, such as LinearTypes. So, in production, it should be necessary to wrap modifiers around custom type constructor to avoid confusion,)
This proof of concept derives Monoid instances based on (optional) modifiers on fields. More precisely, it is a Source Plugin that collects the information of fields with modifiers and saves it as a (private) type family instance.
Then ModifiersOn implements the default implementation based on that type family, and switches the Monoid implementations of fields to use annotated types given by modifiers (via coerce function).
So, for example, the above code:
{-# OPTIONS_GHC -fplugin Data Monoid.Deriving Modifiers #-}
{-# OPTIONS_GHC -Wno-unrecognised-modifiers #-}
module _ where
-- ...
data Foo = Foo
{ total %(Sum Int) :: !Int
, power %(Product Int) :: !Int
, backwards %(Dual [String]) :: [String]
, forwards :: [Int]
}
deriving (Show, Eq, Ord, Generic)
deriving (Semigroup, Monoid) via ModifiersOn (Foo a)
…has representationally the same Monoid instance as follows:
data FooM = FooM
{ total :: !(Sum Int)
, power :: !(Product Int)
, backwards :: Dual [String]
, forwards :: [Int]
}
deriving (Show, Eq, Ord, Generic)
deriving (Semigroup, Monoid) via Generically (FooM a)
… but each field of Foo is not wrapped by newtypes and users can directly access them without ripping them off!
In this way, we can specify the “semantics for instance derivation” and “field value representation” separately. This is in a same spirits of SameRepAs newtype wrapper proposed in DerivingVia paper, but annotated more locally and hence much maintainable compared to manually mapping to the type with the sum-of-product representation.
Although this example is something like a DerivingVia in disguise, but I think this mechanism can further be extended beyond that - for example, as in serde example, I think we can specify field-level encoding strategy for aeson by type-level programming machinery - I think this can also replace some use-case of HKDs.
Although it currently collects modifiers only in field position, but I think it is straightforward to extend it to collect modifiers on data declaration or data constructors.
(One alternative is {-# ANN #-} pragma, but it is more restrictive on where it can be placed and annotation has to be an expression rather than types.)
As this direction seems really promising, it would be more convenient to have builtin mechanism to collect the modifier information through type-level machinery or Template Haskell macro, right builtin to GHC. Otherwise, we had to hand-roll compiler plugin to collect modifier information just as in this example. Still, Modifiers is really a neat and inspiring language feature as it is!
Happy Modified Haskelling!
