4 ms·
> The return type will simply have the form "goodresult | error" and the compiler will infer it based on the function-body alone. I'm 0% convinced that inferre
by duped 3y ago
> The return type will simply have the form "goodresult | error" and the compiler will infer it based on the function-body alone.
I'm 0% convinced that inferred return types are a feature that a production language should have, ever. I've used it before and it's infuriating on top of being bad code.
If a function is annotated f: T -> U where U is not an error, the contract with the caller is that f does not error, and cannot be changed to produce an error, and the compiler upholds this contract for all callers and definitions or redefinitions of f. It also means that another compilation unit can refer to f without having to type check its body, which sure, is a leaky abstraction, but empowers parallel compilation.
Changing the signature is a breaking change and as such should have a higher amount of friction. Changing the error type doubly so.
And on top of all that, errors should not be non discriminated union types. The example is Result<int, int> - you need the error to have a discriminant to distinguish between a success and an error code.
- golergka 3y ago> Changing the signature is a breaking change and as such should have a higher amount of friction. Why? I'm all for very rigorous static type checking. But if we can make refactorings and changes frictionless — let's. Having the ability to move things around and reorder them to work better results in more iterations and better code. As long as each iteration is very rigorously type-checked, of course.
- adamwk 3y agoThe issue is that the API can change unintentionally, breaking clients because of an implementation change.
- naasking 3y agoImplementation changes can already break clients, except without checked errors/exceptions, they do so silently.
- adamwk 3y agoYes, certain language designs make it easier to break APIs for clients, just like the one mentioned above. I was pointing out that this is bad and we should try to prevent that from happening
- naasking 3y agoNo, the one mentioned above makes it easier to not break clients. A language that exports an API without checked exceptions can change the implementation and break clients silently in production. A language with checked exceptions will not break clients silently, ever. That's strictly superior from a robustness perspective. If, as a library author, you want to define a strict contract with clients that you never break, then you explicitly specify the exception signature and you don't let the exceptions be inferred. Then if the implementation changes in a way that introduces a new exception that doesn't match the client contract, then the API author gets the error at compile-time.
- valenterry 3y agoThat depends a bit on the definition I guess - let me explain why: If you call a method in e.g. Java which is annotated to return a string, then by definition you have to expect the following possible outcomes: - The method returns a string - The method throws an exception (or rather: a Throwable) - The method does not return at all That's just how it is in Java and also in most other languages. Therefore, throwing an exception must always be expected. Only if there were only checked exceptions and no unchecked exceptions this would be different - but then how do you handle things like Stackoverflow or OOM exceptions? In fact, if a methods says it returns a string, what that really means is that that it will not return something else than a string. That is the real meaning. It cannot e.g. return an integer. But that's about all the guarantees that it gives. So you see, it's tricky. There are however languages where you can really rely on that they do return what they claim. But those languages then force you to prove that a method actually returns within a finite amount of time and such things. Not very practical to use in many cases.
- golergka 3y agoIt seems that you're talking about a code that is exposed to some third-party clients where you need to maintain compatibility. This is, of course, a valid concern, and you're absolutely right that this layer, exposed to third party clients absolutely does need increased level of friction. However, most applications don't have any such exposure, all of their code is consumed internally. And even when they do, their external API surface which is actually exposed as some REST or ABI is minuscule in comparison to all the internal APIs — method calls and types that are never exposed anywhere. I think that this problem, which is certainly a valid one, should be solved with methods which would not negatively affect developer experience of working with 99% of all the completely private APIs that don't need to be backwards compatible.
- naasking 3y ago> If a function is annotated f: T -> U where U is not an error, the contract with the caller is that f does not error, and cannot be changed to produce an error, and the compiler upholds this contract for all callers and definitions or redefinitions of f The OP is not suggesting that. They're suggesting that U can be an extensible union type that can be inferred from the code. Of course, if you declare it to not be such a union, then you guarantee no errors. Just think of it like checked exceptions from Java, except the exception signature is inferred from the function's code. If you explicitly declare "does not throw", then the compiler produces a type error that you have not handled all exceptions.
- valenterry 3y agoAs others have been said, I think there is a misunderstanding here. First, methods can (and often should) be annotated with their return types. And if a method is annotated as "T -> U" then it cannot return an error and trying to do so would fail the compilation - so I totally agree with you on that. But inside of (non public) application or library code, there are a lot of small methods calling each other before there is some higher-up public method that will be called not by me but someone else. And for those kind of methods type inference is great when it comes to refactoring. Even if I make a mistake and make such a method return an error even though it shouldn't, this mistake will be caught a bit higher up where the return type is annotated. In my experience this has almost no drawbacks and drastically simplifies refactorings.
- duped 3y agoI don't think there's a misunderstanding, I stand by what I said. Inferred return types don't make refactoring easier. In my experience they make it much more difficult, because you have to look at function implementation to understand what it does. Particularly for error handling, you always want to see what errors a function may return even in library code. And all that said, there's no distinction to me between internals and externally facing code when it comes to quality and standards. If you have a function, even if it is called once, it should always have its return type annotated. It is more correct than leaving it to be inferred by the compiler, because you are explicitly annotating the intended behavior of the abstraction. This makes it easier to write correct code and for verifying it by a reviewer.
- jcparkyn 3y ago> Particularly for error handling, you always want to see what errors a function may return even in library code. Any half-decent tooling should do that, since GP was talking about compile-time type inference. Of course there are some situations where the type hints aren't available, but then we're back to a general discussion about type inference. FWIW, I don't actually agree with the idea that set-theoretic unions solve error handling, but I'm fully on-board with opt-in type inference for errors.
- 3y ago