5 ms·
Not sure if I exactly follow, but this is an implementation of Monoid in C#. The interface can be seen as the type-class definition. The structs are the equiv
by louthy 9y ago
Not sure if I exactly follow, but this is an implementation of Monoid in C#. The interface can be seen as the type-class definition. The structs are the equivalent of class instances.
If you look at the `static class Monoid` then you can see a general implementation of mconcat which returns an A and works with Empty and Append.
The Program at the end shows it in use with List and String types.
public interface Monoid<A>
{
A Empty();
A Append(A x, A y);
}
public struct MString : Monoid<string>
{
public string Append(string x, string y) => x + y;
public string Empty() => "";
}
public struct MList<A> : Monoid<List<A>>
{
public List<A> Append(List<A> x, List<A> y) => x.Concat(y).ToList();
public List<A> Empty() => new List<A>();
}
public static class List
{
public static S Fold<S, A>(this IEnumerable<A> ma, S state, Func<S, A, S> f)
{
foreach(var a in ma)
{
state = f(state, a);
}
return state;
}
public static List<A> New<A>(params A[] items) => new List<A>(items);
}
public static class Monoid
{
public static A Concat<MA, A>(IEnumerable<A> ma) where MA : struct, Monoid<A> =>
ma.Fold(default(MA).Empty(), default(MA).Append);
}
class Program
{
static void Main(string[] args)
{
var strs = new[] { "Hello", ",", " ", "World" };
var lists = new[] { List.New(1, 2, 3), List.New(4, 5, 6) };
var str = Monoid.Concat<MString, string>(strs);
var list = Monoid.Concat<MList<int>, List<int>>(lists);
}
}
Obviously it's not as elegant as Haskell, but does this not fit your requirement?
- marcosdumay 9y agoCan you see how that small change on the interface makes it awful to write code that is generic on Monoid? It does not fit the requirements. It makes the kind of code people write in Haskell absolutely not viable.
- louthy 9y agoWhy don't you deal with specifics? I am not talking about its attractiveness, I have already made clear that I think Haskell is more elegant. But, why is this "not viable" or "does not fit the requirements"? I have already shown how a totally generic version of mconcat can be implemented - so how is this not writing "code that is generic on Monoid"?
- marcosdumay 9y agoOne very specific. I create a logging module on Haskell. There I put some high order functions like this: module Log where -- | Runs the code inside a catch, logs any exception exceptions :: IO a -> IO a -- | Logs every code execution access :: IO a -> IO a -- And so on, for several different kinds of logging Then on the main code I do: import qualified Log as Log main = do -- Lots of stuff Log.errors . Log.access $ readSomeData Log.errors $ whateverThatCanFail Log.access $ shoudlntFailButICareAboutRunning On C# each use of those logging functions will be more verbose than copying the entire function body in place. And almost as brittle.
- louthy 9y agoSorry, where did I say C# is less verbose than Haskell? And what has that got to do with implementing polymorphic return values?
- lmm 9y agoThis uses an unsafe "default(MA)" construct to hack around the type system, right? There's no way to write code like this and not have your code fail with NPEs at runtime except for manually inspecting every "default(...)" call to check that it's called on the right kind of type.
- louthy 9y agoWrong, MA is constrained to struct; and structs can’t be null
- lmm 9y agoOk, but someone has to manually check that, since someone could write "default(MA)" with MA not being a struct and this wouldn't be obvious at the call site. And even if we do find a way to automatically enforce that it is a struct, default won't necessarily put it in a valid state, right? (e.g. if the struct contains reference types then we've just moved the problem one step down: the struct can't be null but the things inside the struct can be null). Edit: Also does this "default" mechanism extend to allowing us to compose typeclass instances out of smaller typeclass instances? E.g. the monad instance for Writer is defined as: instance (Monoid w) => Monad (Writer w) where return a = Writer (a,mempty) (Writer (a,w)) >>= f = let (a',w') = runWriter $ f a in Writer (a',w `mappend` w') i.e. we can obtain a Monad<Writer<W, ?>> for any W for which we have a Monoid<W>.
- deleted 9y ago[deleted]
- moomin 9y agoYou're right, that does work, but you're having to declare every type everywhere. Each time I add another operation to your example, the type declarations get worse and worse. My point is, return type polymorphism and ad-hoc polymorphism make a whole bunch of things ergonomic. The fact that C# can do the same thing in a way that no-one would use is kind of the point.
- louthy 9y ago> declare every type everywhere What does that mean? The types are declared exactly once. If you mean C# doesn’t do type inference, well yeah. But, I still don’t see how this changes my original point, which was merely to demonstrate how to do return type polymorphism in a language other than the sacred Haskell. As the original post gives the implication that it’s not achievable when not working in Haskell.
- moomin 9y agoAh, I see the confusion. Shouldn’t have used “declare” in that context. My point was that you needed to specify the type every time you used it. So the more operations you add the more your code looks like generic soup. I think you’re confused as to what return type polymorphism is, though. It’s the ability to have the compiler infer the type of something from its site of use. So your example doesn’t exhibit it because the types need to be specified. So, the following code works in Haskell x = [1,2,read “3”] y = [“1”,”2”, read ”3”] In the first case read returns an Int, in the second a String. This is a useless toy example but it turns out to be really useful and to make ergonomic a huge number of things that are just painful in the languages we normally use. Everything’s achievable in every language, but what’s convenient changes massively.
- louthy 9y ago> I think you’re confused as to what return type polymorphism is, though. It’s the ability to have the compiler infer the type of something from its site of use. I think you may be confused about what return type polymorphism is tbh. Type inference != polymorphism. Polymorphism is a type-system feature. Type inference is a separate process to ascertain concrete types at compile time. For example, the only type inference that C# really does is `var`: var x = foo(); That is no different to me writing: int x = foo(); (assuming that foo returns an int that is). The compiled version of both of those code snippets are the same. The fact that I specified the `int` directly doesn't make those chunks of code any different. Or, make one less valid. You're right it's absolutely the case that to do this I'd need to specify the types I want to work with, and that's because C# is shit at inferring anything. It doesn't change the fact that the return type is polymorphic for Concat in my example. It's _ad-hoc polymorphic_, and no lack of type-inference will change that. But this still misses the entire point of what I wrote. Which was to counter the point made in your original post: > Once you've got return type polymorphism, you really start to miss it in other languages. Initially I felt I was helping by pointing out that if you're missing it in other (mainstream) languages, then there's absolutely a way of doing it. > The simplest example possible is "mempty" mempty :: a > gets the "default" value of a. Which makes no sense in a language where you need an instance to have polymorphism. And secondly I felt it important to use your example of `mempty` to show how to do it.