[ghc-proposals/0232] Ambiguous Type per-signature pragma

Forking from the discussion of How to learn language extensions - #42 by Underlap

Any thought on this pragma, and could anyone shed some light on it’s implementation status?

I think ambiguous types are almost always useless. The most popular use-case seems to be to avoid having to fumble around with Proxy, e.g.:

sizeOf :: forall a. Int
sizeOf = Foreign.Storable.sizeOf (undefined :: a)

intSize = sizeOf @Int

But that has a disadvantage that it is not clear to the user that the type application is required and it gives terrible error messages if you make a mistake.

A better alternative to ambiguous types is just to use a visible forall:

sizeOf :: forall a -> Int
sizeOf (type a) = Foreign.Storable.sizeOf (undefined :: a)

intSize = sizeOf (type Int)

The only problem is that it is not released yet, but it will be in GHC 9.10 (#22326: Visible forall in types of terms, Part 1 · Issues · Glasgow Haskell Compiler / GHC · GitLab).

This also provides another solution to the problematic class in the motivation of that per-signature ambiguity proposal:

class Collects c e  where
  insert :: e -> c -> c
  empty' :: Proxy e -> c

empty :: forall e -> c
empty (type e) = empty' (Proxy :: Proxy e)
2 Likes

Speaking as the author (ahem) I think it’s marvelous. Accepted by the committee coincidentally exactly 4 years ago. Implementation progress: zero. (See a bit more coming-and-going on the discussion thread after acceptance. I believe the ‘proposed syntax for Modifiers’ also has made no progress.)

I think you’ll find plenty on the discussion thread would disagree with that. They always switch it on/they don’t think about it further; they’d find it a burden to annotate each signature that’s ambiguous.

So the proposal is targetted at making life easier for newbies; particularly taking away GHC’s dangerous habit to suggest switching it on – really in the absence of any evidence that’s what the user intended/as opposed to their making a typo.

Yes. That wasn’t a thing 4 years ago. Indeed, as you say, it isn’t a thing yet. (And I do wish someone could find a more convincing example than sizeOf: it’s used so widely in so many libraries/so much legacy code, it’s never going to change.)

1 Like

Here is my perspective as someone not having many years experience in Haskell yet:

  1. For green field projects, I embrace GHC2021. At the same time, for any additional extension I enable in the file or include in cabal project I’d mentally want some justification. And AllowAmbiguousTypes is often one of them.

    I sometimes even separate functions needing AllowAmbiguousTypes to its own module… A per-signature pragma certainly sounds a more hygienic alternative than this.

  2. As a non-native-English speaker, ambiguous sounds overly negative and dangerous. It adds to the additional confusion and hesitancy.

    But this hesitancy is perhaps due to the lack of understanding if it is instead.

So far, I often use AllowAmbiguousTypes to avoid proxy types, along with TypeApplications (in GHC2021).

I think the original thread is quite telling that the lack of clarity of the extensions situation in Haskell create confusion to new developers.

Good to know explicit forall could replace the proxy types or AllowAmbiguousTypes + TypeApplications.

1 Like

I think ambiguous types are almost always useless.

At work we have a bunch of code that is parametrized at the type level and it’s extremely awkward to use a Proxy to work around ambiguity. I prefer to use AllowAmbiguousTypes and TypeApplications. The error messages aren’t really worse than using a Proxy, in fact. If you have

f :: C a => Proxy a -> r
g :: forall a. C a => r

then f Proxy will give you the same “ambiguous type” error message as g so the artificial Proxy argument doesn’t help much. I find it much more ergonomic to write g @a than f (Proxy :: Proxy a).

1 Like

idk if I’d use AllowAmbiguousTypes so much if we had visible forall.

1 Like

Indeed. So if you wrote a signature with ‘ungrounded’ [**] tyvar, that’d be even more likely to be a typo/thinko; then suggesting AllowAmbiguousTypes is even less appropriate.

[**] Trying to think of a less scary word for @hellwolf: by ‘ungrounded’ I mean a tyvar appearing in a constraint but that’s not grounded as the type of an argument. Note that in the first example in the proposal, empty :: c is a method decl, so the effective type is :: Collects c e => c, in which the e isn’t grounded.

So I agree with the objective behind VisibleForall. I find the specific design dysergonomic, and even unHaskelly:

  • The objective is to be able to mark arguments as ‘here for the type only’; and insist that argument be present.
    (Contrast the function caller can provide types via type-@pplication, but those are optional.)
  • Since the oft-repeated example is sizeOf and yet the design is incompatible with the (very long-standing) signature for sizeOf: either we have to switch a gazillion libraries to some newly named function; or we break all those libraries with no easy/gradual deprecation approach.
  • UnHaskelly because it insists some argument appear leftmost.
    (Again @pplications have to come first, but that’s because they’re optional/the syntax has to know when it’s got past them.)
  • UnHaskelly because it gets types appearing where my eye expects a term. Note the initial proposal didn’t include the type herald, and I’m pretty sure the type-acrobats aren’t going to use that ‘feature’ anyway.
  • Also bad for backwards compatibility with Proxy. (I agree with @tomjaguarpaw that Proxy is awkward, but a lot of code uses it. And there’s no guarantee Proxys will be the leftmost arguments.) What would ease transition would be signature saying ‘this argument is here for the type only’ and if the argument type and term is a Proxy (syntactically not a type), interpret that as if VisibleForall.

Anyhoo, this is a long-winded way of saying AllowAmbiguousTypes is going to continue to be needed for a lot longer than if VisibleForall were better designed/with backwards compatibility in mind. Then it’s particularly aggravating the Proposal for per-signature pragma has languished so long after being approved. How hard could it be to at least change the wording of the error message?

If we had visible forall, I think there’s a case for amending the existing ambiguous error to nudge people to use visible forall instead.

2 Likes

Thanks for giving it a thought.

Fwiw, if it is really harmless during compilation time (mostly deferring type error to use sites, right?), perhaps it could be simply with a more positive tone of “Flexible” or something.

But it’s not a big deal after understanding it properly. I wouldn’t split hairs on this too much.

(And I do wish someone could find a more convincing example than sizeOf: it’s used so widely in so many libraries/so much legacy code, it’s never going to change.)

I often use this when writing tests for specific types. E.g.:

testGroup
  "JSON round trips"
  [ testJSONRoundTrip @User
  , testJSONRoundTrip @Address
  ]

I’d be fine with the visible forall arguments, since they’re doing the same as AllowAmbiguousTypes + TypeApplications, making it:

testGroup
  "JSON round trips"
  [ testJSONRoundTrip (type User)
  , testJSONRoundTrip (type Address)
  ]

Though I personally appreciate the type applications more aesthetically. :woman_shrugging:
But I’d still switch over to visible foralls if it’s possible.

You’re in luck. There is a second part to the (accepted) proposal which will allow you to write it like this in the future:

testGroup
  "JSON round trips"
  [ testJSONRoundTrip User
  , testJSONRoundTrip Address
  ]
3 Likes