Calling function outside of module doesn't work with an abstract module signature


#1
/*
  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");

#2

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;


#3

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…


#4

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?


#5

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.


#6

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.


#7

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 :slight_smile: 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.


#10

@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.


#11

Yes, that’s what I implemented.

let nameTypeFromString = (name: string): nameType => name;