Type abbreviations and private fields

The behavior for type abbreviations and the private keyword is difficult to understand.


module Pair = 
    type private pair = int * string
    let f x y : pair = (x, y)

open Pair
printf "%d" (fst (f 3 "Hello"))
(* Prints 3. *)

Why is the design choice behavior here different than for records or discriminated unions?
The compiler forbids me from mentioning the name of the type pair, which seems quite silly! I can’t think of a situation where I would want to control access to the name of the type, but not values of that type.

Welcome to the community patrick-nicodemus.

I believe, and could be wrong, that the use of ‘private’ where you have put it means that the constructor for the type can only be used in the module Pair. This stops people from constructing a ‘pair’ outside of the module and, therefore, bypassing any validation logic.

(I would ‘take this with a pinch of salt’ unless you find it to be correct or someone with more knowledge can confirm it, as I’m still learning myself.)

1 Like

Thanks, Garry. I do not think that is what is happening here.
I’ll post some more sample code to highlight the contrasting behavior with other types.

namespace M
module Modularity =
    type private t1 = int * bool
    let construct_t1 a b : t1 = (a, b) // No compiler error.

    type private t2 = { t2_int : int; t2_bool : bool }
    let construct_t2 a b : t2 = { t2_int = a; t2_bool = b } // This raises a compiler error.
    // The type 't2' is less accessible than the value, member or type 'val construct_t2: a: int -> b: bool -> Modularity.t2' it is used in.

    type private t3 = 
    | I of int
    | B of bool
    let construct_t3 a : t3 = I a // Same compiler error as previously.


module Application = 
    let a1 = Modularity.construct_t1 3 true // No compiler error.
    let a2 = Modularity.construct_t2 3 true // The type 't2' is less accessible than the value, member or type 'val a2: Modularity.t2' it is used in
    let a3 = Modularity.construct_t3 3 // The type 't3' is less accessible than the value, member or type 'val a3: Modularity.t3' it is used in.

It seems to me that it is not that the constructors themselves are safeguarded as I am providing public functions to construct an inhabitant of the type without using the constructors and these are still violating the private/public constraints.

I can imagine that there is an implementation reason for this but on purely language grounds, I cannot see why this behavior is distinct between the different types. Why should type abbreviations be treated differently than type definitions? They are both achieving the same purpose of domain modelling. Even if type abbreviations can be “erased” at compile time and records and discriminated unions cannot, I don’t see why this erasure should happen prior to public/private checking.

Thanks for providing the extra code; that makes the issue much easier to see, and you’re right that it wasn’t what I said it might be.

Unfortunately I don’t know enough about what’s supposed to be happening to say if the errors are expected and, if they are, why they are.

Am I correct in thinking that the question here is not:

  • Why does defining t2 and t3 as private cause an error?

but:

  • Why can t1 be defined as private and not cause the same error as for t2 and t3?

If you define t2 as type t2 = private { t2_int : int; t2_bool : bool } the error (for t2) goes away (notice the placement of private), and I think this was the thing about constructors that I mentioned above which is probably a ‘red herring’. But I don’t think this helps with your question much.

One thing I can say is that type definitions and record field names should normally be in CamelCase.
It’s a bit of a ‘nit-pick’ but having lower case type names looks weird (to me) and makes it harder to figure out what things are.

One other thing I can say is that defining t1 and construct_t1 as:

type private t1 = t1 of int * bool
let construct_t1 a b : t1 = t1 (a, b)

…causes the same error for t1 as for t2 and t3. (In your version of the code I can’t get Intellisense information about t1; it’s like it’s not a ‘thing’ when it’s defined as you have it.)

Maybe that helps you to figure out what’s happening.

Actually, I’m wondering if the reason why there’s no error on t1 could be because it’s an ‘alias’ of a tuple and, since tuples are defined and accessible globally, there can’t be any restrictions placed on where t1 can be used? Maybe the ‘private’ is simply ignored if this is case.

That’s just a thought that popped into my head, rather than reasoned logic or actual knowledge.

I agree with that assessment! It is consistent with what we are seeing so it makes sense as an explanation of the facts. However, I still find it unsatisfying as an explanation from a design point of view.

To me this answer is more of an explanation in terms of a technical feature of the language implementation - type aliases can be erased at compilation and replaced with their definitions, whereas proper “definitions” (records and enums) cannot be, perhaps because they involve new syntactic elements such as the constructors and field projections.

So I am willing to accept this explanation but I am not convinced why as a matter of language design it makes sense to expose privacy and modularity features for records and enums that seemingly cannot be simulated for tuples.

If anything the keyword private should be rejected as inappropriate rather than ignored

Re: camel case, I’m new to the language but the idiom I picked up from similar language was that short vague type names without use of camelcase are fine in situations where the type is only meant to be used in a qualified way and the meaning of the type is obvious from the context of the module it’s defined in.
As an example, in OCaml the string type is “String.t”. If you globally imported the String module this would be quite confusing! But if you always write String.t rather than t it is clear.

Still, I could have chosen better type names here, certainly.

It does sound reasonable to me that, if I define a type to be private within a module, even if it is an ‘alias’ of another public type, the private type should only be available for use within that module.

However, there may be some very good (technical?) reasons why this isn’t so which I simply don’t understand or even know about.

Hopefully someone ‘in the know’ will come along and tell us what’s what, preferably in a simple way that I can understand.

I guess that the choice of case will probably be that of the organisation which owns the code. (If I work in a company which uses all lower-case then that’s what I should use too.)

It’s not a big issue but all of the books about F# that I have say to use camel case for type names and record fields, unless there’s a good reason not to.

One exception that I know about is where the field names need to match exactly – case-for-case – the field names in, for example, a database so that the ‘behind-the-scenes’ functions can extract data and populate the correct fields (and vice versa).

I started off using camel case, as I was told to in the books, so that’s what I have got used to using.