-
Notifications
You must be signed in to change notification settings - Fork 563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow case expressions without branches #2922
Comments
This is mostly an exhaustivity checker issue right now. Type checking should do the right thing. Note you can always use |
I feel like we could sidestep the need for this by either encouraging people to use newtype Z = Z Void
newtype S n = S Void is potentially clearer in intent than data Z
data S n anyway, and switching between these is a non-breaking change as long as you don't export the constructor. The |
I’m actually 👎 on this, personally. The issue of what type we ascribe an empty case declaration seems unnecessarily thorny, since we can already just use Void (or newtypes around it). As already noted, it is also a breaking change since there are a bunch of places people do use |
Does that 👎 extend to changes to the exhaustivity checker? I don't often need truly empty case expressions, but I have found myself in places where I'd much rather write f :: Either Int (Either (Tuple Void String) (Tuple Boolean Void)) -> Int
f = case _ of
Left i -> i than f = case _ of
Left i -> i
Right x -> either (absurd <<< fst) (absurd <<< snd) x or f = case _ of
Left i -> i
Right (Left (Tuple v _)) -> absurd v
Right (Right (Tuple _ v)) -> absurd v The |
We can sidestep the issue of what type to ascribe an empty case expression by raising an error, like GHC does with {-# LANGUAGE EmptyCase #-}
{-# LANGUAGE LambdaCase #-}
module Example where
example = \case {}
Also I don’t think that foreign types which are inhabited but declared with an empty data declarations are a strong argument against empty case expressions since we can already elimitate such types with There’s some unfortunate overlap between both declarations but this is something we should be able to correct when working on promotion, at least this is something we’ll have to think about. |
Presumably |
You can already do everything with |
FWIW I also would like to deprecate the usage of |
Yeah exactly.
Is that really different from monomorphic bindings import Prelude
import Data.Tuple (Tuple(..))
example :: Boolean -> String -> Tuple Unit Unit
example x y = Tuple (f x) (f y) where
f = const unit
module Example where
import Prelude
import Data.Tuple (Tuple(..))
example :: Boolean -> String -> Tuple Unit Unit
example x y = Tuple (f x) (f y) where
+ f :: forall a. a -> Unit
f = const unit or instances resolution? module Example where
import Prelude
import Data.Map (Map, toUnfoldable)
import Foreign.Object (Object, fromFoldable)
example :: forall a. Map String a -> Object a
example = toUnfoldable >>> fromFoldable
module Example where
import Prelude
import Data.Map (Map, toUnfoldable)
import Foreign.Object (Object, fromFoldable)
example :: forall a. Map String a -> Object a
-example = toUnfoldable >>> fromFoldable
+example = (toUnfoldable :: _ -> Array _) >>> fromFoldable
I agree, and I think this is actually a point in favor of empty case expressions since we cannot safely eliminate |
I think there is an important difference from those examples because in those examples you’re providing a type annotation to a subexpression, not the expression itself. I think it’s currently true that in PureScript, if
can ever compile successfully, then it will compile successfully without a type annotation of the form |
I think |
To clarify that: the way I see it, the goal here is to allow people to define uninhabited types with eliminators and without having to resort to unsafe code. For me, recommending newtypes over Void satisfies this requirement, because |
Sorry for quadruple-posting but I just realised I still haven’t really addressed the issue of extending the exhaustivity checker. I’m a bit hesitant of that too because it means that having no constructors becomes part of a module’s public API, even if you don’t want it to be; adding a constructor to a previously empty data type becomes a breaking change if you can write downstream code which made use of the fact that the type previously had no constructors, even if you don’t export the new constructor. |
Yeah, I think making an uninhabited type inhabited ought to be a breaking change, especially if our current guidance for handling uninhabited types is to roll your own If the above argument doesn't move you, though, a compromise where one has to explicitly export |
Ah ok, to clarify I mean that in the case where we implement this, adding a constructor to a previously uninhabited data type becomes a breaking change in a sneaky way that package authors are likely to miss, I think, so without tooling which tells you that you need to release this as a breaking change I think it’s a bit risky (I’d still love to have this tooling but I don’t think we can assume it’ll happen any time soon). |
If we don’t implement this, the only way a type T is observably uninhabited is via a function |
I was ready with a reply until that last comment, which has left me feeling rather confused. I'm probably being slow but could you please explain further? Are you presupposing that any such |
Oh sorry, I should have been clearer! I’m imagining that the recommended way of defining an uninhabited type would be
so that the author of the package containing T doesn’t need to do anything dodgy like defining |
Gotcha, okay. So that's a fine pattern for making sure package consumers can always write safe code for handling Your objection to enhancing the exhaustiveness checker was that adding a constructor to an uninhabited data type would become a breaking change, and in a sneaky way that library authors aren't necessarily going to think about. But the point I was trying to make is that it's already a sneaky breaking change to do so, if downstream authors have written code that is unsafe if and only if the type in question is inhabited (such as rolling their own (If library authors do what you want and wrap |
In total languages you can still define an empty type via So maybe that could be the recommended way of safely making an empty data type? |
I don't find that line of argument particularly convincing personally, because I think if downstream users have already rolled their own |
I think I could get on board with this. How exactly would the compiler decide whether or not a data type is uninhabited, then? Would we build knowledge of Data.Void.Void into the compiler? |
I don't think there's a need for that. There should be a relatively simple heuristic that gets it right for most cases that are likely to be considered in a pattern match:
And then the exhaustiveness checker can ignore any constructors deemed uninhabited by this heuristic. This errs slightly on the side of the compiler thinking that more types are inhabited than they really are, but that's the safe direction in which to err, and I think it correctly handles the vast majority of practical cases. |
Ok, thanks. Those rules seem fine to me, although extending the exhaustivity checker in this way does feel a bit against the spirit of https://github.com/purescript/governance#project-values; to me this feels like a bit of a special-purpose feature, since I think it's quite rare to be writing code that would benefit from this extension. Or to put it a different way, I'm hesitant because I think this would give us a somewhat low bang for our buck (where "buck" is "lines of code in the compiler to maintain"). Do you have a more concrete example of where this would be useful? Do any other core team members want to weigh in on whether they think this is worth including? |
This very well might not be worth the maintenance burden. Speaking as a user and not a collaborator, I just wanted to say that the exhaustivity checker improvement was the part of this I'd be interested in, and I think it's worth considering even if the downsides specific to empty case expressions scuttle that part of the proposal. If there aren't enough industrial users who are excited about that with me to justify it, so be it; it's not essential to anything I'm working on. But to get slightly more concrete, let's say I'm working on my own language, with its own desugaring pipeline. I have an data Expr
= LitStr String
| LitNum Number
| Var String
| App Expr Expr
| Lam String Expr
| Let String Expr Expr And I want to desugar data Expr haslet
= LitStr String
| LitNum Number
| Var String
| App (Expr haslet) (Expr haslet)
| Lam String (Expr haslet)
| Let haslet String (Expr haslet) (Expr haslet)
desugarLets :: Expr Unit -> Expr Void Mission accomplished, except I do still need to write cases for I don't know how often this sort of thing is useful outside of language development. I have the not-necessarily-universal problem that every project I work on for long enough seems to turn into an exercise in language development. |
Can be used for empty data decls such as
Void
. Proposed syntax is to leave off theof
keyword.Type inference may suffer. What would the inferred type of
absurd
be if no explicit type was given?Issue about exhaustiveness checker exists elsewhere.
Not a breaking change, but existing code that uses
data X
as a short-hand forforeign import data X :: Type
must be modified to use the latter; an empty data declaration is now truly an empty type.The text was updated successfully, but these errors were encountered: