13 ms·
Why the fixation on pattern matching? Don't get me wrong, I enjoy the benefit it provides, but for OO code multiple dispatch is a more elegant and idiomatic way
by lacampbell 10y ago
Why the fixation on pattern matching? Don't get me wrong, I enjoy the benefit it provides, but for OO code multiple dispatch is a more elegant and idiomatic way to solve the "I don't want to implement the visitor pattern" problem.
FYI, I recently found out that C# can actually do multiple dispatch
https://blogs.msdn.microsoft.com/shawnhar/2011/04/05/visitor-and-multiple-dispatch-via-c-dynamic/ https://blogs.msdn.microsoft.com/shawnhar/2011/04/05/visitor...
- holtalanm 10y agowow. I havent used C# for at least three years, but that is a really powerful feature.
- zamalek 10y agoWhat's even better is that the DLR consists of some pretty tight code. Historically I was using a ConcurrentDictionary containing Funcs compiled using Expression. I've found the DLR is just as fast (for things that you can express with it), with a much lower overhead. I've started using it in dynamic scenarios wherever possible. Basically, this is going to be about as fast as you can get with the statically-typed underlying runtime.
- zastrowm 10y ago> I've found the DLR is just as fast... with a much lower overhead Can you expand on this? I would not have expected that it would be faster than compiled expressions.
- duncans 10y agoDynamic calls gets compiled at runtime, so there's an initial hit (as there would be with Expr.Compile) but after that ...
- zamalek 10y agoThe codegen creates a cache of delegates (created using expression trees), but uses a structure more suited to the problem than a simple ConcurrentDictionary. [1]: http://geekswithblogs.net/simonc/archive/2012/07/20/inside-the-dlr---callsites.aspx http://geekswithblogs.net/simonc/archive/2012/07/20/inside-t...
- zastrowm 10y agoAh, faster than the `ConcurrentDictionary`. That makes sense!
- zastrowm 10y ago> FYI, I recently found out that C# can actually do multiple dispatch But also good to know, that it can be costly and unsafe. `dynamic` is basically using reflection at run-time, which for most cases is fine, but it's something to keep in mind. It also means that type checking is delayed until run-time instead of compile-time.
- lacampbell 10y agoA dynamic type is probably just implemented as a tagged bit of data, surely. Which is the exact same way an algebraic datatype would be done. So I dunno why it would be any slower. I think the safety thing is a non issue as well - what arguments could you pass in to the statically typed function that would cause the use of dynamic to bite you in the ass?
- zastrowm 10y ago> A dynamic type is probably just implemented as a tagged bit of data, surely A dynamic "type" is really just `object` whose methods are looked up at run time. It's not tagged data at all. See [1] for more info, specifically the example(s) at the bottom, as the reflection code is what gets executed at run (albeit with caching so that methods aren't looked up all the time). `dynamic` is a complex feature that enables multiple dispatch as a side-effect, but only because it allows a whole lot more. [1]: https://visualstudiomagazine.com/Articles/2011/02/01/Understanding-the-Dynamic-Keyword-in-C4.aspx https://visualstudiomagazine.com/Articles/2011/02/01/Underst... > what arguments could you pass in to the statically typed function that would cause the use of dynamic to bite you in the ass public void Output(int value); public void Output(Person person); ... dynamic aValue = "Some String"; Output(aValue); Compiles, because the actual resolving of which overload to call is done at runtime, not at compile time. And at runtime, there is no overload that accepts a string.
- lacampbell 10y agoI am talking about what value you could pass into the multiple dispatch code in the article I linked to. I know you can cause run time type errors using dynamic in general, but in the code I linked it was very well contained and water tight. Dynamically typed languages usually represent objects as something like a struct with an int tag to represent the type, and a void pointer for its value. The actual reflection going on in the code I posted for multiple dispatch would surely be nothing more than an int comparison - same as what Ocaml probably does.
- barrkel 10y agoMultiple dispatch is sometimes more elegant, provided you can divorce the method being dispatched to from the argument classes. If you need to do it with a visitor pattern and multiple callbacks, it's much worse. If you need completeness checking, it's worse. If you've got a simple concise algorithm best expressed in a loop, it's worse. If you've got a nested structure that you want to partially destructure, it's worse. As you've found, overloading is statically typed multiple dispatch; discovering the right overloaded method at runtime is functionally equivalent to multiple dispatch if your overload resolution rules prefer more specific derived types (rather than simply giving up with ambiguity).
- lacampbell 10y agowould you be able to give me a pseudo code example of things that are easy to do with pattern matching, but not multiple dispatch? I'm intrigued, I always considered them equivalent.
- barrkel 10y agoHere's some concrete examples in Rust: http://pzol.github.io/getting_rusty/posts/20140417_destructuring_in_rust/ http://pzol.github.io/getting_rusty/posts/20140417_destructu... It's not just about finding the right bit of code to run; it's also about pulling some fields out of the polymorphic value so they're available as nice short names, without qualification.
- lacampbell 10y agoI suppose languages from the ML family, and rust, haskell etc are somewhat oriented around nested structures of algebraic types that you want to destructure to pull out little atoms of data out of from some well-defined node. But that's orthogonal to the OO approach, where once all the little atoms of data are sealed up in their protective casing of an object you don't want to deal with them anymore. C# is a weird beast though - it's pure OO on some level, but then keeps making getters and setters even easier to write. It's easier to write an "object" where every field has a getter and setter, than it is to write an actual constructor for private variables, so that's what people do.
- robohamburger 10y agoI think matching + destructuring solve similar but different problems. Often times, items you don't need to do something based on some unbounded set of things nor do you need an abstraction. For example, if I am writing a logic system in rust I can either match with one single arm or a nested match implement simple rules like double negation. I don't see how you could easily do this with multiple dispatch like the one you link. I find myself really wish more languages would implement both the system you link and something like rust's/ML's. Usually it's easier to build multi-dispatch than match syntax though. In rust you can use HashMaps of TypeId -> Box<Any>. I would do a similar thing in C# (though now I am going to use this shiny new feature).
- ernst_klim 10y ago> multiple dispatch > "I don't want to implement the visitor pattern" Both multiple dispatch and visitor bring type unsafe dynamism and possible runtime errors. Pattern matching and algebraic types fit well into static type system and are much more safe.
- Sean1708 10y agoWhy does multiple dispatch bring type unsafe dynamism or runtime errors? Or do you just mean C#'s implementation of multiple dispatch?
- hepta 10y agoI think both are not exhaustive (or don't have exhaustiveness enforced by the compiler).
- ernst_klim 10y agoYep, there is no way to check the exhaustiveness in the compile time. Say you have an Expression parent class and a list of child classes: say Add, Const, Var, and a corresponding visitor. Add new child class, and the visitor would be non-exhaustive. Multiple dispatch is not very different in that sense. I think that Alan Key spoke about dynamic nature of OOP, that's one of the examples. Even in the statically typed environment OOP demonstrates its dynamic nature.
- Sean1708 10y agoI'm sorry would you mind going into a bit more depth about that example? It's not obvious to me why the compiler can't figure out the the visitor is no longer exhaustive.
- ernst_klim 10y agoVisitor is just a collection of functions for all type's subtypes. The problem is that visitor knows nothing about class' subtypes, just like the base class knows nothing about it's children, so you have to add visit cases manually for each child class. The only way to detect that visitor is non exhaustive is to apply it to the child class it has no visit function for.
- alkonaut 10y agoTo me it seems pattern matching without possibility to actually enforce that all cases are matched seems to miss the most useful scenario: What I want to do is create proper sum types. For some reason most OO languages were designed around the idea that being "open for extension" was a good thing. Normally if I make a base class, I want a closed and known set of sub classes. E.g. if I make a payment method then I want to make a Credit or Invoice kind where one requires Credit Card details and the other requires an address. In F# that's a simple sum type. In C# making a sum type is a convoluted mess of making an outer base class and private nested sub classes. Here a sum type with the two cases plus an exhaustive pattern matching switch would have solved in 10 lines what takes me 300 lines to do in C#. I think the concept of "fixed" or "closed" hierarchies is foreign enough to OO that it probably needs to be a completely separate concept from the normal types. F# shows that it can be implemented as sugar on top of the regular CLR type system. All that's needed is that some keyword such as "enum class" is converted into a sealed hierarchy of plain record classes with no methods on them. The switch statement we already have can then treat these "enum classes" specially by checking that all cases are matched. A sketch solution for C# could look like enum class PaymentMethod { CreditCard(CrediCardDetails cc), Invoice(Address billingAddress), } // later switch (paymentMethod) { case CreditCard(ccdetails): Console.WriteLine($"Creditcard expiring {ccdetails.ExpiryDate}"); case Invoice(addr): Console.WriteLine($"Bill to {addr.Name}"); } Writing the above in current C# would require significantly more boilerplate. This isn't the best pattern to use in all situations: but in many cases when the types are merely data carriers it doesn't make sense to put the logic in the class as OO prescribes, so the FP recipe works much better.
- Latty 10y agoScala gets this right, in my opinion, with sealed traits and case classes. "enum classes" feel like a much more limited option by comparison. Although I'd take either over the status quo to be sure.
- alkonaut 10y agoWhat's the difference between the enum class I outlined and scalas case classes? I don't know Scala but reading the docs on its case classes, their description seem to fit exactly what I tried to describe (immutable types, closed for further inheritance etc). The example they give for notification = email | sms | voice looks a lot like what I was trying to sketch with the paymentMethod example. http://docs.scala-lang.org/tutorials/tour/case-classes.html http://docs.scala-lang.org/tutorials/tour/case-classes.html I didn't intend for my proposal to be more limited than than the Scala case classes at least :). I want exactly that. An FP closed type hierarchy with enforced pattern matching.
- pron 10y agoNo fixation. It can just be more convenient sometimes (and less convenient in others).