Characterizing the type of functions that can accept `()` as input (without monomorphizing)
Asked Answered
C

1

8

Here are a few simple functions:

f1 :: () -> ()
f1 () = ()

f2 :: a -> a
f2 a = a

f3 :: a -> (a, a)
f3 a = (a, a)

f4 :: (a, b) -> a
f4 (a, b) = a

All of f1, f2, and f3 are able to accept () as an input. On the other hand, of course, f4 can't accept (); f4 () is a type error.

Is it possible to type-theoretically characterize what f1, f2, and f3 have in common? Specifically, is it possible to define an acceptsUnit function, such that acceptsUnit f1, acceptsUnit f2, and acceptsUnit f3 are well-typed, but acceptsUnit f4 is a type error -- and which has no other effect?

The following does part of the job, but monomorphizes its input (in Haskell, and I gather in Hindley-Milner), and hence has an effect beyond simply asserting that its input can accept ():

acceptsUnit :: (() -> a) -> (() -> a)
acceptsUnit = id

-- acceptsUnit f4     ~> error 😊
-- acceptsUnit f3 'a' ~> error ☹️

The same monomorphizing, of course, happens in the following. In this case, the annotated type of acceptsUnit' is its principal type.

acceptsUnit' :: (() -> a) -> (() -> a)
acceptsUnit' f = let x = f () in f
Crwth answered 27/7, 2021 at 20:27 Comment(12)
I don't think this is possible, though I'm not sure I could prove it. Why do you want this? The more context you can give us, the more likely we are to be able to propose alternatives. – Rumpf
I don't think you can get a satisfactory answer. In Hindley-Milner, when you take a function as input, it becomes monomorphic (roughly speaking). Haskell goes a little beyond that, but I don't think it helps much also because we don't really have impredicative types as in System F. There might be some partial answer using something like Typeable on a rank-N type (?) but I'm really unsure about that. – Albert
It's related to my earlier question. I need to be able to characterize when a program in a DSL with an indexed-state monad semantics is, in effect, a 'closed term' -- that is, I want to make sure that a certain operation can apply to such 'closed terms', and not to others. One way to characterize such terms is that they can accept () as an input state. Another way (seen in the other question) is to treat closed terms as ones whose value doesn't depend on the input. – Crwth
It's clearly impossible in Haskell, but I don't know enough to prove it. In System F (plus datatypes and case), the question doesn't even make sense: the only functions that can accept () are ones whose first argument is of type (). – Spy
@SimonC Why does that determination need to be done from within the language? Why isn't making that determination in the metalanguage good enough? Also: I think it would be helpful to have even more context. Are you trying to sell cat food or go to the moon? What program are you writing to enable that? What is the operation that needs this wacky type that must exist for the program to exist, and why? – Rumpf
Could you use Template Haskell? I don't know how to do so, but you could use that to write a macro that applies a function to the an argument twice (acceptUnit' a a) ignoring the types. Then acceptUnit can take a from-unit argument as its first one and the real argument as its second. – Spy
@Albert we have ImpredicativeTypes in GHC 9.0.1, but I don't think that helps. – Haily
@DanielWagner Well, it's a bit complicated. I'm researching semantics for natural language, so the DSL is in effect...English. The idea is to give a tight characterization of certain patterns of possible pronoun-antecedence relationship along lines described in this work, but in a more general setting for anaphora (roughly, the linked paper uses a Reader monad, but natural language really requires indexed State). One can do this if an operation that 'terminates' interpretation is restricted in the way I'm asking about. – Crwth
@DanielWagner I'd be interested to hear more about how you intend 'in the metalanguage', but the idea behind the approach I'm taking is that all and only attested interpretations should be well typed. This is a very common approach in natural language syntax and semantics (1, 2). – Crwth
Sorry, are you asking "is there a language and a typing of that language (ideally an extension of Hindley-Milner) that allows me to characterize dependence of a function on one of its arguments, or allows me to specify that a polymorphic argument could be unified with unit?" If so, how about asking that question? It's not a great SO question, though. Maybe ask on a lambda-the-ultimate.org forum? – Dominickdominie
@K.A.Buhr I took myself to be asking something close to the latter of those. Another question linked in my earlier comments asks something close to the former. – Crwth
"In the metatheory" looks like adding a new kind of term to your DSL's syntax, say, takes_unit, with a typing rule that looks like "if Ξ“ ⊒ e : t and Ξ“ ⊒ e : () -> t' then Ξ“ ⊒ takes_unit e : t". (Can't render premises above conclusion in markdown, hopefully you can understand what I intend here.) Of course you'd have to think about whether there's an algorithm that corresponds with that modified type system, but it seems pretty likely to me that there would be one. – Rumpf
D
4

It's easy to type-theoretically characterize what f1, f2, and f3 but not f4 have in common. In the language of Hindley-Milner, the first three have polytypes that can be specialized to a polytype of the form:

forall a1...an. () -> tau

for n >= 0 and tau an arbitrary monotype. The fourth one can't.

Can you write a function that accepts the first three as an argument but rejects the fourth? Well, it depends on the type system you're using and the latitude you have to structure your function. In the usual Hindley-Milner and/or standard Haskell type system, if you have latitude to pass two copies of your candidate function to the acceptance function, the following will work:

acceptsUnit :: (() -> a) -> (b -> c) -> (b -> c)
acceptsUnit = flip const

f1 :: () -> ()
f1 () = ()

f2 :: a -> a
f2 a = a

f3 :: a -> (a, a)
f3 a = (a, a)

f4 :: (a, b) -> a
f4 (a, b) = a

main = do
  print $ acceptsUnit f1 f1 ()
  print $ acceptsUnit f2 f2 10
  print $ acceptsUnit f3 f3 10
  -- print $ acceptsUnit f4 f4  -- type error

That's probably the best you'll be able to do with standard Haskell (and probably the best you can do with Haskell plus GHC type system extensions, or someone would have found something by now).

If you're free to define your own typing system with its own typing rules, then the sky is the limit.

Dominickdominie answered 29/7, 2021 at 19:16 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.