Replies: 5 comments 14 replies
-
This is a very interesting idea. I am certainly taking this seriously as a concept. And, if it's truly general, or at least general enough to significantly reduce the boilerplate, then I'm all for it.
Yeah, the idea is to use source-generators. The source-generator would read the interfaces hanging off the trait-type and use that to auto-generate the non- This was also going to be my approach to reducing the boilerplate in the trait-implementation, but your idea is better in my opinion. |
Beta Was this translation helpful? Give feedback.
-
+1 for this idea! However, I think that framing these interfaces as That being said, while the usage of monad-transformer-stack should be an implementation detail, the fact that the domain-monad-type behaves identically to the monad-transformer-stack is definitely not! Consumers of the domain-monad-type must necessarily rely on its behavior and if it happens to behave like some other monad, there's really no issue in relying on that equivalence. In fact, knowing this equivalence can aid in documenting and teaching that particular domain-monad-type. Therefore, perhaps it would be better to frame this not as a "wrapper" but instead simply as an "equivalence"? The term that immediately comes to mind is "isomorphism" i.e. there's a lossless conversion between the two types. I could imagine rewriting your public interface Isomorphism<F, G>
{
static abstract K<G, A> Transform<A>(K<F, A> fa);
static abstract K<F, A> Transform<A>(K<G, A> ga);
} Admittedly, this boils down to just renaming the interface and its members. However, I do think this is a better way of framing the relationship between the domain-monad-type and it's monad-transformer-stack. If you change the underlying stack of Additionally, |
Beta Was this translation helpful? Give feedback.
-
@louthy, that sounds good, like I mentioned I don’t have much real experience with v5 overall so it makes a lot of sense for you and anyone else who’s actively utilizing it to feel things out for a while. I'll hold off on additional code contributions, especially given @hermanda19's rename proposal. @hermanda19 Very interesting idea! My first thought was that it would be great to be able to implement that interface as The next thing I noticed was that changing static K<Self, B> Applicative<Self>.Apply<A, B>(K<Self, Func<A, B>> mf, K<Self, A> ma)
=> Self.Wrap(Self.Unwrap(mf).Apply(Self.Unwrap(ma))); to static K<F, B> Applicative<F>.Apply<A, B>(K<F, Func<A, B>> mf, K<F, A> ma)
=> F.Transform(F.Transform(mf).Apply(F.Transform(ma))); Again, “familiarity” is alway a tricky question because the approachability of something to others is a real factor, but I also don’t want to conflate “familiarity to me” with some kind of objective standard. Overall I think your rename has much of the force of principle and consistency behind it, but I’m interested in seeing how if/how any discussion on the question of “familiarity” plays out. |
Beta Was this translation helpful? Give feedback.
-
So, I tried a few of the ideas from the comments above, and ended up back at the
I tried this out with the This is the code before: public partial class Game :
Monad<Game>,
SemigroupK<Game>,
Stateful<Game, GameState>
{
public static K<Game, B> Bind<A, B>(K<Game, A> ma, Func<A, K<Game, B>> f) =>
new Game<B>(ma.As().runGame.Bind(a => f(a).As().runGame));
public static K<Game, B> Map<A, B>(Func<A, B> f, K<Game, A> ma) =>
new Game<B>(ma.As().runGame.Map(f));
public static K<Game, A> Pure<A>(A value) =>
new Game<A>(Prelude.Pure(value));
public static K<Game, B> Apply<A, B>(K<Game, Func<A, B>> mf, K<Game, A> ma) =>
new Game<B>(mf.As().runGame.Apply(ma.As().runGame).As());
public static K<Game, A> Combine<A>(K<Game, A> lhs, K<Game, A> rhs) =>
new Game<A>(lhs.As().runGame.Combine(rhs.As().runGame).As());
public static K<Game, Unit> Put(GameState value) =>
new Game<Unit>(StateT.put<OptionT<IO>, GameState>(value));
public static K<Game, Unit> Modify(Func<GameState, GameState> modify) =>
new Game<Unit>(StateT.modify<OptionT<IO>, GameState>(modify));
public static K<Game, A> Gets<A>(Func<GameState, A> f) =>
new Game<A>(StateT.gets<OptionT<IO>, GameState, A>(f));
public static K<Game, A> LiftIO<A>(IO<A> ma) =>
new Game<A>(MonadIO.liftIO<StateT<GameState, OptionT<IO>>, A>(ma).As());
} This is the code after (using public partial class Game :
Deriving<Game, StateT<GameState, OptionT<IO>>>,
Deriving.Monad<Game, StateT<GameState, OptionT<IO>>>,
Deriving.SemigroupK<Game, StateT<GameState, OptionT<IO>>>,
Deriving.Stateful<Game, StateT<GameState, OptionT<IO>>, GameState>
{
public static K<StateT<GameState, OptionT<IO>>, A> Transform<A>(K<Game, A> fa) =>
fa.As().runGame;
public static K<Game, A> CoTransform<A>(K<StateT<GameState, OptionT<IO>>, A> fa) =>
new Game<A>(fa.As());
} It's hard not to see that as an elegant solution (especially as there's no performance hit either). Anyway, have a look, take it apart, throw any critique ... If you're going to suggest an alternative approach (which I am open to!), please make sure the result is as elegant as the code above -- or close. If there's significantly more overhead then any new benefits become moot. |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Disclaimer I've not used v5 or the custom domain monad pattern in production at all, so this may not be as useful as I'm imagining.
Playing around with V5 locally, I've noticed that thing domain monad wrappers often result in a lot of repeated "wrapping" and "unwrapping" boilerplate when implementing trait methods, independent of the underlying types. Upon observing this and playing around locally, I've come up with some interfaces that seem to be quite useful in initial testing
It's pretty trivial to write these for
Readable
,Stateful
,Fallable
,Foldable
etc. as well.I stole the term "Deriving" from Haskell's
deriving
syntax, which I also have little to no production experience with, so there may be a more accurate and/or appropriate term to use.As a usage example
Once
Wrap
andUnwrap
are implemented, you can attach as manyDeriving
behaviors as your inner type will support. I've also been experimenting locally with providing "building block" types such asFallibleReaderT
andFalliableRWST
that can help domain monads to more easily derive multiple behaviors. For example, givenas a pre-existing utility type, our custom domain type above can get a lot more functionality, if needed
Having these "Deriving" interfaces also opens up some interesting possibilities not strictly related to domain monads. For example, "convenience" versions of types like
Validation
that default toSeq
rather than allowing any monoid, akin to what we find in V4 (This is something my team at work would likely want if we ever switched to V5)and even things like easily defining new types to capture constraints such as "non-empty"
Anyway, I think that's enough demonstration for now. Does this seem like something worth pursuing @louthy? Like I said, I don't have direct experience with the domain wrapper monad pattern yet, but I can imagine it could be difficult getting buy-in for curious-but-FP-skeptical teams adopt said pattern, especially when there are so many other new concepts to learn with v5.
This also doesn't help at all with generating methods and special
SelectMany
overloads for the concrete types, but as far as I can tell code generation is basically a necessity for that.I have most of the work for these done already on my local machine, and would be more than happy to open a pull request, or even create a separate library if I'm feeling up for that and we don't want these in Core (although like I mentioned I think they could help v5 adoption, at least somewhat)
Beta Was this translation helpful? Give feedback.
All reactions