Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Using "Maybe" as a positive example of what Haskell can do isn't right. Say you have a function with input of type A and output of type B, written (A -> B). The problem with Maybe ("option" types) then is that if you have a function, in production use, of type (X -> Maybe Y), you can't "weaken your assumptions for the input and strengthen your promises for the output" (which would be an improvement) and rewrite it to the type (Maybe X -> Y). Because then you would have to modify all the code which already uses the function. Since "A" and "Maybe A" are incompatible types. Which is illogical.

Several other null-safe languages solve this correctly by allowing disjunctions of types (often called unions, though countless other type related things are also called unions). They have a type operator "|" (or) and the function type (X -> Y|Null) can be improved by rewriting the function to (X|Null -> Y). Code outside the function doesn't have to be changed: Accepting X or Null implies accepting X, and returning Y implies returning Y or Null.



> if you have a function, in production use, of type (X -> Maybe Y), you can't "weaken your assumptions for the input and strengthen your promises for the output" (which would be an improvement) and rewrite it to the type (Maybe X -> Y).

If your value of Y is predicated on receiving an X, I have trouble seeing how you would write such a function. If you have a default value, then Haskell would solve it just like any language with optionals:

    y :: Int -> String

    (fromMaybe "defaultValue" (map y (Just 3))
> Several other null-safe languages [...] returning Y implies returning Y or Null.

I have trouble seeing how the language is null-safe in that situation.


> If your value of Y is predicated on receiving an X

We didn't assume it is. Say you have a function of type (String -> String|Null). Further assume that you realize you don't necessarily need a String as input, and that you in fact are able to always output a string, no matter what. This means you can rewrite (improve!) the function such that it now has the type (String|Null -> String). Relaxing the type requirements for your inputs, or strengthening the guarantees for the type of your output, or both, is always an improvement. And there is no logical reason why you would need to change any external code for that. But many type systems are not able to automatically recognize and take advantage of this logical fact.

> > Several other null-safe languages [...] returning Y implies returning Y or Null.

> I have trouble seeing how the language is null-safe in that situation.

If you always assign a value of type Y to a variable of type Y|Null, the compiler will enforce a check for Null if you access the value of the variable, which is unnecessary (as the type of the variable could be changed to Y), but it can't result in a null pointer exception.


Don't mean to appear as talking down to you, but the "relaxation" or "strengthening" that you talk about exactly corresponds to either (1) changing the function that you use at the call site, or (2) changing the "external code" function. The thing you call "improvement" sounds like a plain type error to me.


Then you are misunderstanding something...


Yeah I also really don't understand the point that's being made here: it looks like a great way to introduce more errors.


The mainstream is languages that will happily accept null as anything, and crash at runtime. Sure, union types are cool, but they aren't expressible in most languages, while the optional construct is.

Haskell's type system definitely is a positive example of what can be done to avoid completely the null problem. Is it the utmost that can be done? No. But it's been a working proof of solution for 20 years, while proper typecheckers for union types are a recent thing.


Yeah but there are arguably different standards for Haskell. Haskell's advanced type system is one of its main selling points, so it doesn't make sense to explain the benefits of Haskell with a case (Maybe) where its type system falls short (no "or" in types).


> Haskell's advanced type system is one of its main selling points, so it doesn't make sense to explain the benefits of Haskell with a case where it's type system falls short.

Falls short compared to what? Arguably, if you're talking to someone using Java or Python, Maybe is plenty enough; and getting started on type families is certainly not going to work well.


These languages don't have null safety. Haskell does have null safety, but at the cost of the additional complexity that comes with Maybe wrapping. So it's not as unambiguously an improvement as union typing is (which adds less complexity but still grants null safety).


> Several other null-safe languages [...] returning Y implies returning Y or Null.

If `Y` is implicitly `Y|Null`, then it is no longer possible to declare "this function does not return null" in the type system. Now understanding what a function can return requires checking the code or comments. This is the opposite of null safe.


> If `Y` is implicitly `Y|Null`

It isn't. It's just that if you say "this function returns Y or null", and it returns Y, your statement was true. If you give me a hammer, this implies you gave me a hammer or a wrench.


It must. If it is possible to rewrite `X -> Y|Null` as `X|Null -> Y` without changes to external code, then the `X` type needs to accept `X|Null` and the `Y` type needs to accept `Y|Null`, therefore any `T` must implicitly be `T|Null` and the language is not null safe. Result types are what you get when you require explicit conversions.

I may still be thinking about this incorrectly. Do you have an language in mind that contradicts this?


You seem to think `X -> Y|Null` and `X|Null -> Y` have to be equivalent, but that's not the case. The second function type has a more general input type and a more restricted return type. And a function which can accept X as an input can be replaced with a function that can accept X or Null (or X or anything else) as input type. And a function which has can return types Y or Null (or Y or anything else) can be replaced with a function that can return type Y. Old call site code will still work. Of course this replacement only makes sense if it is possible to improve the function in this way from a perspective of business logic.

> I may still be thinking about this incorrectly. Do you have an language in mind that contradicts this?

Any language which supports "union types" of this sort, e.g. Ceylon or, nowadays, Typescript.


I get it! (Thanks, playing around with actual code helped a ton.) For example, in Typescript you're saying you can add a default value simply:

    # old
    function f(x: number): number {
        return 2 * x;
    }

    # new
    function f(x: number|null): number {
        x = x || 3;
        return 2 * x;
    }

    # usage
    # old
    f(2)
    # new
    f(2) # still works!
But in Haskell this requires changing the call sites:

    -- old
    f :: Int -> Int
    f = (*2)

    -- new
    f :: Maybe Int -> Int
    f = maybe 0 (*2)

    -- usage
    -- old
    f 2
    -- new
    f (Just 2) -- different!
But I actually feel this is an antipattern in Haskell (and maybe TypeScript too), and a separate wrapper function avoids refactoring while making things even more user friendly.

    -- old
    f :: Int -> Int
    f = (*2)

    -- new
    fMaybe :: Maybe Int -> Int
    fMaybe = maybe 3 f

    -- usage
    -- old
    f 2
    -- new
    f 2 -- still works!
    fMaybe Nothing -- works too!
Here's some wrappers for general functions (not that they're needed, they're essentially just raw prelude functions):

    maybeIn :: b -> (a -> b) -> (Maybe a -> b)
    maybeIn = maybe

    maybeOut :: (a -> b) -> (a -> Maybe b)
    maybeOut = fmap Just

    maybeBoth :: b -> (a -> b) -> (Maybe a -> Maybe b)
    maybeBoth d = maybeOut . maybeIn d
Added bonus, this approach avoids slowing down existing code with the null checks we just added.


Got `maybeOut` wrong, can't edit, but it should be:

   maybeOut :: (a -> b) -> (a -> Maybe b)
   maybeOut = (.) Just
Also the parenthesis around the last two output types are added for emphasis, but can be safely removed.


Last reply. Probably. Here's `maybe` in TypeScript:

    const maybe = <T,>(f: (_: T) => T, d:T) => (x: T|null) => f(x || d);

    console.log(maybe((x) => 2 * x, 3)(null)); // returns: 6


This came to mind while considering your interesting point: After such a change, wouldn’t you feel the urge to inspect all users of the stricter return type and remove unnecessary handling of a potential null return?


I don't know about such urges. But sometimes there is no possibility to inspect all user code, e.g. when you are providing a library or API function.


Good point. In such case I would probably consider leaving the signature as is, even after tightening, and possibly offer a function with stricter signature for new code to use while deprecating the older variant. This would inform the users without rug pulling.


But that's not necessary in a language with union types of this sort. No rugs being pulled.


data Maybe a = Nothing | Just a deriving (Eq, Ord)

>> Because then you would have to modify all the code which already uses the function, as "A" and "Maybe A" are incompatible types. Which is illogical.

I'm not so sure about your statement. If the type of the function changes, revising its usage at every use point is good for your sanity. I would go further and say that sometimes Maybe X is too weak, because it doesn't contain precise semantics for its Nothing and Just x alternatives. For example, sometimes you want a `Nothing` for a value that hasn't yet been found, but could potentially be filled up the evaluation chain, e.g. `NothingYet`, and a different Nothing for a value that is conclusively not there, e.g. `TerrifyingVoid`. If you fork your `Nothing` value into these two variants after you discover the need for it, you will have to revise each call anyway, and check what's the proper course of action. And this is a feature I wish I could use from Python.

More generally, in large Haskell code bases, having the type-checker help you track, at compile time, code that breaks far away from where you made your changes, is an incredible time-saver.


Yes, there are edge cases where you would like to have multiple different "kinds of null", but those use cases seem so uncommon in practice that they are mostly irrelevant.


If you have a value "aValue :: a" and a monadic function of "mFunc :: (a -> Maybe b)" that's essentially just asking you to use `>>= :: Maybe a -> (a -> Maybe b) -> Maybe b` as well as `pure :: Applicative f => a -> f a` which will lift our regular `aValue` to a `Maybe a` in this instance.

Then to get the result "b" you can use the `maybe :: b -> (a -> b) -> Maybe a -> b` function to get your "b" back and do the weakening as you desire.

`Maybe` assumes a computation can fail, and the `maybe` function forces you to give a default value in the case that your computation fails (aka returns Nothing) or a transformation of the result that's of the same resultant type.

Overall, you'd end up with a function call that looks like:

foo :: b

foo = maybe someDefaultValueOnFailure someFuncOnResult (pure aValue >>= mFunc)

or if you don't want to change the result then you can use `fromMaybe :: a -> Maybe a -> a`

bar :: b

bar = fromMaybe someOtherDefaultValueOnFailure (pure value >>= mFunc) -- if the last computation succeeds, return that value of resultant type of your computation


This is fine and understandable in theory, but a usability disaster in practice.

If function f returns b or nothing/error, and is then improved to return b always, client code that calls f should not require changes or become invalid, except perhaps for a dead code warning on the parts that deal with the now impossible case of a missing result from f.

You are suggesting not only a pile of monad-related ugly complications to deal with the mismatch between b and Maybe b, which are probably the best Haskell can do, but also introducing default values that can only have the practical effect of poisoning error handling.


> If function f returns b or nothing/error, and is then improved to return b always, client code that calls f should not require changes or become invalid

Why do you need to change the type signature at all? You "improved" [1] a function to make impossible for the error case to occur, but it's used everywhere and the calling code must handle the error case (I mean, that's what static typing of this sort). So there you have it: the client code is not rendered invalid, it just has dead code for handling a case that will never happen (or more usually, this just bubbles up to the error handler, not even requiring dead code at every call site).

As an aside, I don't see the problem with the "pile of monads" and it doesn't seem very complicated.

----

[1] which I assume means "I know I'll be calling this with values that make it impossible for the error to occur". If you are actually changing the code, well, it goes without saying that if the assumptions you made when choosing the type changed when re-writing the function, well... the calling sites breaking everywhere is a strength of static typing.


Changing the type signature (which, by the way, could be at least in easy cases implicitly deduced by the compiler rather than edited by hand) allows new client code to assume that the result is present.


Changing the type signature to relax/strengthen pre or post conditions is a fundamental change though. I would expect it to break call sites. That's a feature!


Strengthening postconditions and relaxing preconditions is harmless in theory, so it should be harmless in practice.

Haskell gets in the way by facilitating clashes of incompatible types: there are reasons to make breaking changes to type signatures that in more deliberately designed languages might remain unaltered or compatible, without breaking call sites.


> If function f returns b or nothing/error, and is then improved to return b always, client code that calls f should not require changes or become invalid, except perhaps for a dead code warning on the parts that deal with the now impossible case of a missing result from f.

You can achieve this by not changing the type and keeping the result as Maybe b. Dead code to handle `Nothing`, no harm done.

However, you clarify you don't want this because:

> Changing the type signature (which, by the way, could be at least in easy cases implicitly deduced by the compiler rather than edited by hand) allows new client code to assume that the result is present.

But this cuts both ways. If the old call site can assume there may be errors (even though the new type "b" doesn't specify them) then the new call site cannot assume there are no errors (what works for old must work for new).

I must say I see no real objection to the proposal at https://news.ycombinator.com/item?id=41519649 besides "I don't like it", which is not very compelling to me.


(or absent in the case of input parameters)


A function in which the input is needed for the computation is very different to one where it's not needed. I would expect the type signature to reflect this, why would you want it otherwise?


Say you have a function which expects objects of type Foo as an input and which returns objects of type Baz. One day, the function is improved by also accepting the type Bar, i.e. Foo|Bar. So Foo isn't needed for the computation, because Bar is also accepted.

Or you have a function which expects objects of type String as an input. But then you realize that in your case, null values can be handled just like empty strings. So the input type can be relaxed to String|Null.


There's a difference between empty strings and Null values imo.

Just "" != Nothing

If you want to handle empty strings as a input in haskell then you have a function of type `f :: String -> b` and you pattern match on your input?

  f "" = someResult
  f ...
Nothing assumes a proper null in that there is genuinely nothing to work with. Still you can make a function to handle it or use `maybe`?


Perhaps that theoretically solves the problem, but it sounds awfullly complicated in practice.


So in your hypothetical language with union types `X | Null -> Y` is a function that can actually return `Y | Null`? Why would you want to allow that as an implicit behavior? This would make for surprising and unclear error handling requirements.

One of the main points of encoding error information in the type system in the type system is that it forces you to account for it when you modify your code.

By "weakening" your assumptions on your function to allow it to produce Null values you have introduced a new requirement at all your call sites. Everywhere that you call this function now needs to handle the Null value. Its a GOOD thing that Haskell forces you to handle this via the type system.


> So in your hypothetical language with union types `X | Null -> Y` is a function that can actually return `Y | Null`?

No, but a function which returns Y | Null can be replaced with a function which returns Y without changing code on the call site.

Imagine I always used to give you a nail (Y) or nothing (Null), and you can handle both receiving a nail and receiving nothing. Then I can, at any time, change my behavior to giving you nails only. Because you can already handle nails, and the fact that I now never give you nothing doesn't bother you. You just perform a (now useless) check of whether you have received a nail or nothing.


> a function which returns Y | Null can be replaced with a function which returns Y without changing code on the call site.

Yes this falls out of injectivity.

> the function type (X -> Y|Null) can be improved by rewriting the function to (X|Null -> Y)

I agree that any value received by the former function (`X`) can be received by the latter function (`X|Null`). However you cannot rewrite the former to have the signature of the latter.

You would need to write:

prf : (X -> Y|Null) -> X|Null -> Y

You would have to be able to convert a `Null` value into a `Y`.

You could definitely use `X|Null -> Y` to implement `X -> Y|Null` but that is not what you are claiming.


> However you cannot rewrite the former to have the signature of the latter.

Of course I can. I can always change the code to anything I like. That's what "rewriting" is. The question is only whether the business logic still makes sense, and whether the old call site code still works. Just look at the example I gave above.

> You could definitely use `X|Null -> Y` to implement `X -> Y|Null` but that is not what you are claiming.

"Implementing" is a special case of rewriting, so how can you say you can implement something but not rewrite it?


> Of course I can. I can always change the code to anything I like. That's what "rewriting" is. The question is only whether the business logic still makes sense, and whether the old call site code still works. Just look at the example I gave above.

You have a function that can return a Null response and you are claiming you can rewrite it to be one that does not return a Null.

This means that in the cases where your function previously produced a `Null`, you have to produce a `Y`. You claimed you can do this if you write the function to receive `X|Null`. In other words you are claiming you can write `(X -> Y|Null) -> X|Null -> Y`. I challenge you to write this function.

> "Implementing" is a special case of rewriting, so how can you say you can implement something but not rewrite it?

I didn't say that. You claimed you can write `(X -> Y|Null) -> X|Null -> Y`. I am saying that is impossible but you could write `(X|Null -> Y) -> X -> Y|Null`. Do you see the difference?


You seem to have a strangely specific understanding of the term "rewrite". "Rewriting" here just means replacing one function with another one. If it is possible to write a function f, then any(!) function g can replaced with f simply by renaming it. That's the same as "rewriting". It's just writing a different function and giving it the same name. The question is only if your call site code will still work (from the compiler perspective, we ignore the business logic here). It generally will still work if the name of the new/rewritten function stays the same. But it also will still work if the input type is more general or the return type is more specific, or both. That is, if your language supports union types of this sort (like e.g. Typescript).


Scala seems to be the only language that recognizes the merit of having both unions and ADTs. It even has GADTs!


Haskell also has GADTs: https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/gadt..., firstly as an extension and now it's built into the latest version of this year's compiler


You're misinterpreting 'GHC2024'. It's just a language edition, a short hand of enabling a bunch of extensions. You have been able to enable GADTs for many years now, with just a single pragma. It has been built in to GHC for all these years.


Yep, Scala is influenced by Haskell.


Yet ironically Scala is not null safe I believe.


I have been doing scala professionally for a decade, I have probably have 1 NPE per year on average, max. And pretty much never from an established scala lib. It's by convention not to use null - heavy use of Option, etc.



Opt-in is better than nothing, but in practice I assume this sees little use because it breaks compatibility with old code. Null safety (and type safety in general) has to be present in a programming language from the start; it can't realistically be added as a feature later.


I don't think I agree. C# added it and its gone well and Java is adding null safety without breaking backwards compatibility at all.


If anything, it's a default to have them on in any reasonably recent project - you get that with all templates, default projects, etc. Actively maintained libraries are always expected to come with NRT support. If this is not the case, it's usually a sign the library is maintained by developers who ignore the conventions/guidelines and actively went out of their way to disable them, which usually a strong signal to either file an issue or completely avoid such library.

Similar logic applies to code that has migrated over to .NET 6/8, or any newly written code past ~.NET 5.


Types in old code may be nullable, or not, the compiler doesn't know, so the only way the compiler can ensure null safety for using old code is by enforcing you to do null checks everywhere. That's not very practical. Moreover, the old code itself may still produce null pointer exceptions.


But that doesn't break compatibility like you claimed and it also doesn't support your conclusion that it will not likely be used.

> he compiler doesn't know, so the only way the compiler can ensure null safety for using old code is by enforcing you to do null checks everywhere

This isn't necessarily true. Java's approach is to have 3 types: nullable, non-null, and platform (just like Kotlin). Platform types are types without nullness markers and don't require strict null checks to prevent backwards compatibility breaking. Yes, old code may still product null pointers, but we don't need 100% correctness right away. At some point platform types will be 1% of code in the wild rather than 100%.


> At some point platform types will be 1% of code in the wild rather than 100%.

In the case of Java this could take decades. Or people simply continue to write platform types because they are lazy (the compiler doesn't force them). Then platform types will never decrease substantially.


I don't think that's true and I don't think there is any data to back that up. We've already seen in the C# community rapid adoption of nullness markers. This whole goal post moving and the idea that if we can't have 100% we shouldn't do it at all is a bit exhausting so I think I'm done here. Cheers man.


Well yeah, there is no way around this on the JVM. That's one of the JVM's problems/drawbacks. Everything can be null and everything can throw exceptions.

But in practice, as long as you use only Scala libraries and are careful when using Java libraries it's not really an issue. (speaking as a professional Scala developer for many many years)


I don't use Haskell, so this is a dumb question. Why can't X be implicitly cast to Maybe X?


Haskell/GHC tells you what the types are. Proper, global, most-general type inference. Not that local inference crap [1] that the sour-grapes types will say is better.

You lose this ability if you start letting the compiler decide that `Int` is as good as `Maybe Int`. Or if an `Async (Async String)` may as well be an `Async String`.

That's not to say it's not easy to transform (just a few keystrokes), but explicit beats implicit when it comes to casting (in any language).

[1] Does this work?

  var x = 1 == 2 ? Optional.of(2L) : Optional.empty().map(y -> y + y);
  // Operator '+' cannot be applied to 'java. lang. Object', 'java. lang. Object'
How about

  Optional<Long> x = 1 == 2 ? Optional.of(2L) : Optional.empty().map(y -> y + y);
  // Operator '+' cannot be applied to 'java. lang. Object', 'java. lang. Object'
or even

  Optional<Long> x = 1 == 2 ? Optional.of(2L) : Optional.empty().map((Long y) -> y + y);
  // Cannot infer functional interface type
No, we needed Optional.<Long>empty() instead of Optional.empty() to make it work


> letting the compiler decide that `Int` is as good as `Maybe Int`

I was thinking more like explicitly telling the compiler that an implicit cast is OK, in other languages done by implementing the implicit cast operator for example.

edit: but if I understood you correctly, Haskell just doesn't support any implicit casting?


It will do some wrangling of literals for you, as long as it can unambiguously decide on an exact type during type-checking.

If no other info is given, it will treat `3 + 3` as Integer + Integer (and emit a compiler warning because it guessed the type).

With `(3 :: Int64) + 3`, the right 3 will resolve to Int64. Same if you swap their positions.

`(3 :: Int64) + (3 :: Int32)` is a compile error.

"Text literals" can become a String, a Text, or a ByteString if you're not explicit about it.

> implicit cast operator

Wouldn't that make it explicit?


> Wouldn't that make it explicit?

No, the casting is still done implicitly. That is I can make the following compile fine in Delphi if I add an implicit cast operator to either Foo or Bar:

    Foo x := Foo.Create();
    Bar y := x;
If neither of them have a suitable implicit cast operator defined, it will of course fail to compile.

Just an example, nothing unique about Delphi. You can see an example of the operator definition here[1].

[1]: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Oper...


> "Text literals" can become a String, a Text, or a ByteString if you're not explicit about it.

Not without explicitly enabling `OverloadedStrings`!

https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/over...


It can using pure or return or if working with just Maybe specifically then Maybe is defined like so:

data Maybe a = Just a | Nothing

So to make an X a Maybe X, you'd put a Just before a value of type X.

For example:

one :: Int

one = 1

mOne :: Maybe Int

mOne = Just one -- alternatively, pure one since our type signature tells us what pure should resolve to.

Reason we can do this is because Maybe is also an Applicative and a Monad and so implements pure and return which takes an ordinary value and wraps it up into an instance of what we want.


Sounds similar to how you need to do Some(x) when passing x to something expecting an Option in rust.

Swift interestingly doesn’t require this, but only because Optionals are granted lots of extra syntax sugar in the language. It’s really wrapping it in .some(x) for you behind the scenes, but the compiler can figure this out on its own.

This means that in swift, changing a function from f(T) to f(T?) (ie. f(Optional<T>)) is a source-compatible change, albeit not an ABI-compatible one.


Isn't that explicit casting? Implicit casting would be automatically performed by the compiler without the need to (re)write any explicit code.


> mOne = Just one

I'd call that explicit casting. Implicit casting would be

    mOne = one
Compiler already knows what "one" is, it could insert the "Just" itself, no? Possibly due to an operator defined on Maybe that does this transformation?

That is, are there some technical reasons it doesn't?

Or is it just (no pun inteded) a language choice?


Why would this be useful? Why do you want the types to change underneath you?


Better question: Why would you want your call site code to break when your type signature gets changed in a way that doesn't necessitate breaking anything?


Because what you're asking for precludes the concept of mathematical guarantees. I'm not taking your question at face value, because you could be asking why call site code should break when the type signature generalises (which is a useful thing), but that's not what you're asking.

It seems you're asking for code to be both null safe and not null safe simultaneously.

Having a language just decide that it would like to change the types of the values flowing through a system is wild. It's one of the reasons that JavaScript is a trash fire.


You are misunderstanding things.


I’m certainly misunderstanding why so many people in this thread insist on speaking authoritatively on a topic they clearly know very little about.


Because it otherwise forces the caller to have an extra explicit step that doesn't really contribute to anything. It's a trivial transform, and as such just gets in the way of what the code actually does.

Of course with great power comes great responsibility, so it's a tool that should be used sparingly and deliberately.

Now as mentioned I don't use Haskell, but that's why I like it in other languages.

I asked as I was curious if there was something that prevented this in Haskell, beyond a design choice.


A good reason to use Haskell is that it generally guides the programmer away from doing things like this.

If you make a breaking change to your API, then you should want your tools to tell you loud and clear that it’s a breaking change.

I also don’t agree that keeping the structure around values internally logically consistent “doesn’t really contribute to anything”. On the contrary, I think this idea is hugely important. How would your idea generalise? The compiler should just know that my `Int` should be a `Maybe Int` and cast it for me. Should the compiler also know that my `[a]` should be cast to a `(a, b)` because incidentally we’re fairly confident that list should always have two elements in it?

I think if this way of thinking is unfamiliar, then it’s a good reason to learn Haskell (or Elm, which is at least as good, or maybe better, for driving this point home).


> The compiler should just know that my `Int` should be a `Maybe Int` and cast it for me.

The compiler should not "just" know it. It would know it because we told it how.

Consider a function that takes a float and returns a complex number. I then change the function to take a complex number ("Complex Float" in Haskell if I read the docs right), and returns a float.

I could then tell the compiler, by implementing an implicit cast operator, how to cast float to complex. The implicit part is then that the compiler tries it without me telling it to use the cast explicitly.

Then any code that worked with the old function should work perfectly fine using the modified function without modifications, since per definition the reals are contained in the complex numbers.

This is how I do it in several languages I've used.


But now you’re talking about something else aren’t you? Now you’re talking about generalising. You can generalise, for example, from Float to Floating a => a. But I don’t understand how the original Int to Maybe Int change could be sensible. How does that work?


The exact same way? Or perhaps you could tell me, in which cases can an Int not be turned into a Maybe Int?

Again, I don't know Haskell, so from the outside it looks like much the same as the float -> complex conversion.


It sounds like you need to learn about parametricity.

https://www.well-typed.com/blog/2015/05/parametricity/


It doesn't sound like that to me. Could you not just answer his question?


To whom shall I send the invoice?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: