I’m trying to write a generic gauss solver that works with floats, rational numbers, etc. As a prerequisite to write a suitable functor, I try to create the following module types and modules (below formatted for utop):
#require "zarith";;
module type Number = sig
type t
val lt : t -> t -> bool
val is_zero : t -> bool
val sub : t -> t -> t
val mul : t -> t -> t
val div : t -> t -> t
end;;
module FloatNum : Number = struct
include Float
let lt x y = x < y
let is_zero x = x = 0.
end;;
module QNum : Number = struct
include Q
let is_zero x = Q.sign x = 0
end;;
But I get an error message that doesn’t make sense to me:
21 | let is_zero x = Q.sign x = 0
^^^^^^^^
Error: This expression has type int but an expression was expected of type t
Is this a bug or am I overlooking something?
Strangely, I can work around the problem like this:
+let q_is_zero x = Q.sign x = 0;;
+
module QNum : Number = struct
include Q
- let is_zero x = Q.sign x = 0
+ let is_zero = q_is_zero
end;;
Can someone reproduce this and/or explain to me why Q.sign x is expected to be of type t in the one case but not in the other?
P.S.: I’m using utop version 2.16.0, OCaml version 5.4.0, and zarith version 1.14.
Yes, this is because you include Q and Q redefines = to compare t with t. But Q.sign returns an int thus the type doesn’t match the expected signature:
Oh, that makes sense. I wonder, however, how do I write this idiomatically then? Like this?
module QNum : Number = struct
include Q
let is_zero x = Stdlib.(=) (Q.sign x) 0
end
Or this?
module QNum : Number = struct
let is_zero x = Q.sign x = 0
include Q
end
In the latter case, if Q contained such a function one day (maybe with different semantics, which is unlikely of course), it would override my definition.
I generally sometimes wonder how to be forward compatible with includes and open. Consider, for example:
let x = Q.one;;
assert Q.(Q.zero + x = Q.one);;
What if Q would add a function x one day? That would shadow my local variable, right? (Again, unlikely, but I’m more wondering about the hypothetical problem here.)
#require "zarith";;
module type Number = sig
type t
val lt : t -> t -> bool
val is_zero : t -> bool
val sub : t -> t -> t
val mul : t -> t -> t
val div : t -> t -> t
end;;
module FloatNum : Number = struct
module Ext = struct
let lt x y = x < y
let is_zero x = x = 0.
end
include Float
include Ext
end;;
module QNum : Number = struct
module Ext = struct
let is_zero x = Q.sign x = 0
end
include Q
include Ext
end;;
This looks quite ugly, and I doubt it’s what you would idiomatically do, but doesn’t this pattern have the following advantages?
I can use any types or functions in scope without needing to worry if Q might define them in the future.
My definitions will take precedence over those in Q (including those added in future).
Beware that this signature constraint make the module QNum unusable since it makes the type QNUm.t an abstract type that can never be constructed. Signature constraints should be used to remove information, and not to check if a module implements an interface.
Thanks for pointing that out. I noticed meanwhile that I would need Number with type t = Q.t here, but generally, is the following better then?
-module FloatNum : Number with type t = float = struct
+module FloatNum = struct
include Float
let lt x y = x < y
let is_zero x = x = 0.
end;;
-module QNum : Number with type t = Q.t = struct
+module QNum = struct
include Q
let is_zero x = Int.equal (Q.sign x) 0
end;;
What is better? I would assume : Number with type t = … is keeping my exposed API cleaner. But not really sure what is common practice.
In general yes. There’s no need for the type constraints in cases where modules are implementing signatures. If you want to make 100% certain you can also do module _ : Number = FloatNum and so on separately after defining the modules.
I solve this by not doing open of modules unless they have specifically been designed for opening (e.g. Infix operator modules for let* or >>= or something like that) or I specifically want an include to override my definitions (e.g. when I backport stdlib functions and want these to be replaced by the Stdlib versions if available).