Skip to content
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

Incoherent syntax between types and functions #7149

Closed
vicuna opened this issue Feb 15, 2016 · 9 comments
Closed

Incoherent syntax between types and functions #7149

vicuna opened this issue Feb 15, 2016 · 9 comments
Assignees

Comments

@vicuna
Copy link

vicuna commented Feb 15, 2016

Original bug ID: 7149
Reporter: MonsieurPi
Assigned to: @gasche
Status: closed (set by @xavierleroy on 2016-12-07T10:29:43Z)
Resolution: not a bug
Priority: low
Severity: feature
Fixed in version: 4.02.2
Category: ~DO NOT USE (was: OCaml general)
Monitored by: @diml @yallop

Bug description

To declare a recursive function, we need to write

let rec f x = ... (* some call to f *)

To declare a type (recursive or not) we always write

type t = ... (* here we can mention t *)

My problem is this :

If I want to write

type t = A | B
type t = C of t | D

which would allow me to make the first type t unavailable outside of the second type, I can't do it.

Maybe I could write

type t = let type t = A | B in C of t | D

but no, it's not allowed.

First of all, the declaration of recursive type is not coherent with the declaration of recursive functions and, second, the absence of a "rec" keyword doesn't permit me to do whatever I want with types :'(

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: @gasche

Since 4.02.2, OCaml supports a "nonrec" keyword in type declaration precisely for this:

type t = A | B;;

type t = A | B

type nonrec t = C of t | D;;

type nonrec t = C of t | D

C A;;

  • : t = C A

Note that with older OCaml versions you can often work around the lack of "nonrec" by declaring type synonyms:

type t = A | B;;

type t = A | B

type u = t;;

type u = t

type t = C of u | D;;

type t = C of u | D

C A;;

  • : t = C A

or use modules for scoping

module M = struct

type t = A | B

end;;
module M : sig type t = A | B end

include M;;

type t = M.t = A | B

type t = C of M.t | D;;

type t = C of M.t | D

C A;;

  • : t = C M.A

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: MonsieurPi

Note that with older OCaml versions you can often work around the lack of "nonrec" by declaring type synonyms:

type t = A | B;;

type t = A | B

type u = t;;

type u = t

type t = C of u | D;;

type t = C of u | D

C A;;

  • : t = C A

I don't like the use of synonyms because I don't want the first t to be visible (even with the alias u).

For the first solution, I didn't know it but it still remains incoherent (I think) with the function declaration. But well, at least the feature exists ;-)

Thanks

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: @gasche

For the first solution, I didn't know it but it still remains incoherent (I think) with the function declaration.

It's hard to evolve a language -- and I find your remark a bit insensitive in that it ignores this difficulty.

ML historically had both recursive and non-recursive let-bindings (using "let" and "let rec"), but only recursive type bindings (using "type"). The situation since 4.02.2 is more symmetric, as we also have recursive and non-recursive type binding (using "type" and "type nonrec").

My personal preference would be to have "rec" and "nonrec" available for all binders (so we could write "let nonrec x = 1" and "type rec ulam = Lam of ulam -> ulam", and also "module rec" and "module nonrec", ec.), and have a "default recursivity", for each binder, depending on which form is used more often. For "let", "nonrec" would be the default, so a simple "let" would be understood as "let nonrec", and "type" would be "type rec". This is consistent and convenient.

In practice adding nonrec was already a controversial move in terms of backward compatibility, and the language was extended minimally (no support for "type rec", no support for "let nonrec") to minimize unplanned consequences.

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: MonsieurPi

It's hard to evolve a language -- and I find your remark a bit insensitive in that it ignores this difficulty.

Oh, yes, sorry. I thought about retro-compatibility and I didn't want to sound insensitive because I know it's hard. What I was trying to say is that, for example in case of teaching, this kind of incoherence can sometime be a blocking moment. But I guess it's like your brother in law, you have to deal with it. :-D

I completely agree with your personal preference. :-)

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: MonsieurPi

Nevertheless, it's not working as expected

If I write

let f x = x + 1;;

val f : int -> int =

let f x = x +. 1.;;

val f : float -> float =

and then

f 3;;

Error: This expression has type int but an expression was expected of type
float

the first declaration of f is hidden.

Now, if I write

type t = A | B;;

type t = A | B

type nonrec t = C of t | D;;

type nonrec t = C of t | D

And, then

A;;

  • : t = A

C A;;

  • : t = C A

And even worse :

let f = function

  | A  -> ()
  | C A -> ()
  | _ -> ();;

Error: This variant pattern is expected to have type t
The constructor C does not belong to type t

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: @gasche

It seems to me that this is working as expected.

I'm not sure what you mean by "make the first t unavailable"; if the type is unavailable, should its constructors also be unavailable? But then, how would you expect to use the (C of t) constructor of the second type, if the first is "unavaiable"?

You could try using

module M : sig type t end = struct
type t = A | B
end
include M
type nonrec t = C of t | D

which makes t an abstract type (its definition is not exported in the signature of M). You cannot talk about A or B outside M, but that severely restricts the usefulness of the second type t.

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: MonsieurPi

Mmmh, I would agree with you but what I wanted to do was the following :

Define an algebraic type t then define some functions to handle it

Define another algebraic type t then define some functions to handle it.

What I'm sure of, at this point, is that the first defined type t can not be managed by other functions than the previously defined ones.

For example

type t = A | B

let f = function
| A -> print_string "I'm A"
| B -> print_string "I'm B and my programmer is original";;

let create i = if i > 0 then A else B;;

type nonrec t = C of t | D
let f = function
| C t -> f t
| D -> print_string "I'm D and I cannot see A and B without C";;

let create i j = if j > 0 then C (create i) else D;;

so, the previous defined functions f and create are not visible, only the ones defined for the second type. I don't have to create a module for this.

But at this moment, I can still do

f (C A);;

and I expect ocaml to tell me

Error: This variant expression is expected to have type t
The constructor A does not belong to type t

(which is what happens if I write

let f x y = x + y;;

val f : int -> int -> int =

let f x = f x 1;;

val f : int -> int =

f 2 3;;

Error: This function has type int -> int
It is applied to too many arguments; maybe you forgot a `;'.
)

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: @gasche

We have other ways to refer to a type than its name (for example, we can use its constructors); this mean that hiding or shadowing the name does not suffice to prevent a type from being used. If you want to ensure that "the first defined type t can not be managed by other functions than the previously defined ones", you have to create a module, put the functions in this module, and use the module interface to enforce your preferred usage restrictions.

(It is possible to allow matching over values of the type, but not creating new values, using "private" types. See the OCaml manual for more details.)

@vicuna
Copy link
Author

vicuna commented Feb 15, 2016

Comment author: MonsieurPi

Ok. I agree with that.

I still find strange that we have to types named t. Didn't know this was possible.

Thanks for all the answer, I'll keep using my modules, then ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants