9 ms·
I feel obsessing over types is often just a form of procrastination, as it feels more interesting than the real work that needs to be done. This seems to be a b
by beefsack 6y ago
I feel obsessing over types is often just a form of procrastination, as it feels more interesting than the real work that needs to be done. This seems to be a bigger issue in Rust because the type system is powerful.
I write a lot of Rust nowadays, so I often need to keep myself in check and make sure I'm not getting sidetracked. When I'm writing internal code, the priority is to just get it done but still avoiding shortcuts. This is mainly avoiding .unwrap() / .clone() / taking care to handle Result correctly, as well as regularly running clippy to pick up the obviously silly stuff.
I find the most painful part of this strategy is when you want to revisit completed code to reduce copying as that generally requires a lot of type changes. At least when you get to this point though and haven't taken shortcuts, you've got functioning software and it should be fairly fault tolerant.
- DaiPlusPlus 6y ago> I feel obsessing over types is often just a form of procrastination, as it feels more interesting than the real work that needs to be done. I spent 2 days implementing a (terrible) discriminated union struct type in C# (for types I don’t control, so I can’t just add an interface) rather than just return `Object` from a couple of methods. Type systems need to be more expressive if we’re going to be able to increase developer productivity without costing program safety.
- moreaccountspls 6y agoOr, you know, just return Object from a couple of methods. It's not the end of the world.
- rob74 6y ago...or interface{} in Go
- pwm 6y agoThere are expressive type systems around, it’s the industry at large that needs to use them more and educate future generations on why these are useful tools. I 100% agree that spending 2 days on that task is lunacy. But imagine if youcould do it instantly with 0 ceremony. At that point it would 100% worth your while.
- DaiPlusPlus 6y agoSo, part of those 2 days was spent toying with Roslyn code-generation to try to re-implement C++ templates in C# - but I ran out of time and reverted back to using T4. The .NET CLI itself places limits on what .NET-based languages can actually do: for example, structural-typing at method boundaries in CIL is only possible by having the compiler generate build-time adapter classes and interfaces - which means that methods accepting structural-types can’t be easily called by non-strucutural-typing-aware languages. The CLI also imposes single-inheritance - so a .NET language cannot implement multiple inheritance at all - and because non-virtual calls cannot be intercepted at all (without hacks like abusing .NET Remoting’s special-cases in the CLR) it’s not possible to have a “every call is virtual” system like Java’s in the CLR (note to self: find out how J# did it). Implementing Go-style composition is hamstrung by the fact we can’t patch the vtable at runtime either (reified vtables would be nice...). I dare say it - but I think 2020 marks the decline of C# and the CLR because we’re held-back by decisions made over 15 years ago. .NET 5 isn’t showing any sign of significant improvements to the CLR’s type system or fundamental concepts.
- dragonwriter 6y ago> I spent 2 days implementing a (terrible) discriminated union struct type in C# A type system without native or at least low-impedance support for discriminated unions is, well, not a very good type system. > Type systems need to be more expressive if we’re going to be able to increase developer productivity without costing program safety. Lots of type systems support discriminated unions directly, or at least make it trivial to implement them. C# perhaps doesn’t, but that's a problem with C#, not typed programming in general.
- CGamesPlay 6y ago> I feel obsessing over types is often just a form of procrastination, as it feels more interesting than the real work that needs to be done. This seems to be a bigger issue in Rust because the type system is powerful. This is interesting, because I generally feel like getting the types right actually IS most of the work that needs to be done, and once the types are correct the implementation code just follows from the available operations necessary. I've never used Rust, so perhaps it's different in this language (I understand memory management is quite onerous there?), but this is how things feel for me in C# or TypeScript.
- zadler 6y agoTry Haskell, the ceiling is so high for the type system that you can spend a lot of time trying to golf an interface until you are happy that it is maximally elegant. Banal is better in some regards, hense Go. But it’s always a trade off where the extremes are pushing your boundaries and getting stuff done.
- eru 6y agoGo is making the wrong trade-offs. My example for something that's simpler than Haskell but useful would be OCaml.
- jacobr1 6y agoWhich design choices made for OCaml do you think would be worthwhile to consider making in Go?
- eru 6y agoI wasn't thinking of any specific design choices, but rather the overall feeling of pragmatism one gets from OCaml when coming from Haskell. Most of my favourite design choices are the ones that make OCaml a functional programming language. So mostly not apt for Go. But here's a list of what I think one could adapt while staying true to the idea of a C-like language: C had unions. They had some obvious problems. Go's solution was to get rid of them. OCaml instead went for discriminated unions (https://www.drdobbs.com/cpp/discriminated-unions/240009296 https://www.drdobbs.com/cpp/discriminated-unions/240009296) and made the compiler check that you are handling all cases correctly. In OCaml parlance, they are called algebraic data types. C had null pointers. Go inherited them. OCaml uses the general mechanism of discriminated unions to represent cases like 'end of a linked list' or 'no data' or 'unknown' instead. And the compiler will check for you that you either handled those cases (when declared) or that you don't create those cases (when not explicitly declared to be there). Some of C's constructs are generic. Like accessing arrays or taking the address of a value or dereferencing a pointer. To give an example, p works for a pointer of any type and does the right thing. And the compiler will tell you off, if you mix up the types. As a user of C you can not add your own generic operations. You can use copy-and-paste or cast to and fro void or sometimes use function pointers in a clever way. Go goes the same route. For example the built-in datastructures are generic, like maps or arrays. Similar to C, a user of Go can not add their own generic operations. Go's sorting https://golang.org/pkg/sort/#Interface https://golang.org/pkg/sort/#Interface demonstrates what I mean by clever use of function pointers. (Note, how even that sorting interface still has lots of copy-and-paste when implementing and how it only really works for in-place sorting algorithms. I'm not sure how much trouble it would give in a multi threaded environment.) I suspect Go's aversion to user-creatable generics comes from C++. C++'s templates confuse ad-hoc polymorphism and parametric polymorphism. If C++'s unholy mess were the only game in town, Go's stance of restricting generics to the grownups (ie language implementors) would be perfectly reasonable. OCaml carefully separates these two kinds of polymorphism. Parametric polymorphism basically means 'this function or data structure works identically with any type'. That's what we also call generics. Eg the length of an array doesn't depend on what's in the array. (See https://en.wikipedia.org/wiki/Parametric_polymorphism https://en.wikipedia.org/wiki/Parametric_polymorphism) When compiling you only need to create code once regardless of type. (Modulo unboxed types.) Examples of ad-hoc polymorphism (https://en.wikipedia.org/wiki/Ad_hoc_polymorphism https://en.wikipedia.org/wiki/Ad_hoc_polymorphism) are things like function overloading to do different things for different data types. An infamous example are the bitshift operators from C whose cute looks got them roped into IO duty in C++. In OCaml parametric polymorphism is handled as the simple concept it is. Ad hoc polymorphism (and unboxing for parametric polymorphism) are more complicated, and rightly make up the more complicated bits of OCaml types system. Go could have gone with parametric polymorphism and left out ad-hoc polymorphism to stay simple. (Parametric polymorphism synergises well with algebraic data types. Eg you can model the possibility of a null-pointer via an 'option' type, and have generic functionality to handle it. Same for the slightly richer concept of a value that's 'either an error message or a proper value'.) But discussions about generics are a bit of a mine-field in Go. Type inference. Go's type inference only goes one way, and is very limited. OCaml type inference goes backwards and forwards. Rust's system is perhaps a good compromise: it has similar machinery to OCaml, but they intentionally restrict it to only consider variable usage information from inside the same function. (Generics restricted to parametric-polymorphism-only don't make type inference any harder.) Back to something uncontroversial: Go allows tuples and pattern matching against them only when returning from functions and has lots of specialised machinery like 'multiple return types'. OCaml just allows you tuples anywhere any other value could occur. You can pattern match against tuples and arbitrary other types. The compiler ensures that your pattern matching considers all cases. I am sure there are more things to learn. But those few examples shall suffice for now. I feel especially strong about tuples as a wasted opportunity that wouldn't impact how the language feels at all. (From a practical point of view discriminated unions would probably make a bigger impact.)
- franga2000 6y ago> I feel obsessing over types is often just a form of procrastination Can confirm, this happened to me in Java constantly. I once spent an hour writing a big utility class using generics, even though I needed it for only one type in the code. "What if I need it for something else? Copying the code and replacing the types is very inefficient!" Turned out that Java, being Java, was unable to instantiate an empty array of a generic type, so the very elegant code in the utility class now meant having to handle a bunch of special null cases in the application. I actually left it in for a while, but thankfully changed my mind before merging it as I'm sure the team wouldn't have appreciated having to deal with even more null in unexpected places because I got attached to my shiny generic code.
- eru 6y agoThat sounds like a problem with Java more than a problem with generics. In general, I agree with your sentiment. Though in some cases, an abstraction can be justified even when used only once. Take the simple case of a function. Generally a function only communication with its surroundings via parameters and returned values. A function that respects those conventions makes reading code much easier, and can be worth it even if used only once. In contrast, a code block inside a much larger stretch of code doesn't give the reader any explicit guidance one how it intends to interact with the rest of the system. Even if it's exactly the same code as in the function. Similarly, a for-loop can do almost anything. But using map or filter immediately tells you that only a few things are possible here. Even a fold is more restricted than a general for-loop. 'Theorems for free' makes a very similar argument for generics. https://people.mpi-sws.org/~dreyer/tor/papers/wadler.pdf https://people.mpi-sws.org/~dreyer/tor/papers/wadler.pdf
- alkonaut 6y agoFor me that type golfing is how I explore the problem. A lot of times you realize things about the problem that you didn’t understand before. This is especially true with Rust where you also need to consider who owns what data. With C# it’s much too to model things that make sense as a type zoo but where the more important problems like ownership and access patterns is lost in the cracks. The typical sign in Rust is when you need a lifetime on a struct so you realize it has someone else’s data. That usually means I stop to think before progressing. In C# it’s very easy to miss that big red flag. Obviously it’s always worth revisiting and simplifying a design, but I still don’t think the initial design or designs were a waste of time if it gave me an understanding of the problem space.