/*
Calling function Person.make outside of module Person doesn't work with an abstract module signature.
*/
module type P = {
type greeting;
type nameType;
/*
type name = string; /* This works, but then it's not abstract. */
*/
let greet: nameType => greeting;
};
module Person: P = {
type greeting = string;
type nameType = string;
let greet = (name: nameType): greeting => "Hello " ++ name;
};
/*
"John" is marked with the following error message:
"Error: This expression has type string but an expression was expected of type Person.nameType"
*/
let p = Person.greet("John");
Calling function outside of module doesn't work with an abstract module signature
Yup this is exactly what should be happening. Look at the type of greet
: let greet: nameType => greeting;
To call greet
, you need an argument of type nameType
. How do you get that? nameType
is abstract, so you can’t just use a string
. You have two options: make the type definition of nameType
public (string
), or add a function in the module type P
that converts a string
to a nameType
, and use that: let nameTypeFromString: string => nameType;
But the module Person defines this nameType as a string, and if the line “let p = Person.greet(“John”);” was inside module Person, then this would work without the need for a function to convert it…
Yes, that’s what module types do. They define what is and needs to be exposed. If you’re more familiar with interface files, a module type is exactly the same as an .rei
file.
What are you actually trying to accomplish by making the type abstract, if not to hide its implementation?
To put it another way, abstract types are not interchangeable with their representation types from the perspective of a caller. If you need it to be interchangeable, then make it concrete, not abstract.
Thanks!
I understand the concept of module signatures, so it’s logical that the types either has to be open or there has to be a conversion function.
Trying the conversion function, I added
let nameTypeFromString: string => nameType;
to the signature, but then things became a little strange. In the Person module any of the following works:
A. let nameTypeFromString = (name) => name;
B. let nameTypeFromString = (name: string): nameType => name;
C. let nameTypeFromString = (name): string => name;
A and B make sense, but how come C works? Is it because the underlying type of nameType is string?
I am also not sure if this conversion function is the proper/idiomatic way to cast (I couldn’t find this word in relation to Reason) or convert between types, but it works.
Inside the Person
module, the compiler knows and allows you to use string
and nameType
interchangeably. Anything inside a module can see and use anything else defined previously in the same module.
Conversion functions are indeed idiomatic. We have lots of them in the OCaml world just in the standard library you will see things like int_of_string
, string_of_int
, and so on. Usually we put a type and its related functions in its own module, so e.g. you’d have a Name
module with a type t
and conversion functions fromString
and toString
.
@mendel I think it might be helpful to see an example of this:
Suppose you wanted a type that is essentially an int
but can only represent numbers between 0 and 100. In your module signature you could have:
type hundo;
let hundo_of_int: int => hundo;
let int_of_hundo: hundo => int;
Now the type checker can help you ensure your values are in the correct range.
Yes, that’s what I implemented.
let nameTypeFromString = (name: string): nameType => name;