failwith
is the standard function to raise a general error, but it's a little bit clumsy to use because it only takes a fixed string.if temp >= 100 thenIf you want to have the message contain useful debugging information, you need to use
failwith "we've reached boiling point"
sprintf
to generate the fixed string, like:if temp >= 100 then(I'm assuming here that you have
failwith (sprintf "%d degC: boiling point reached" temp)
open Printf
at the top of your file, something which you should almost always do so you don't need to write Printf.sprintf
all the time,).With this simple tip we can turn
failwith
into a function that automatically takes a printf-like format string, and we can learn a little bit about the arcana of polymorphic types too.First of all, here is the code:
let failwith format = ksprintf failwith formatYou can see in the toplevel that it works:
# let failwith format = ksprintf failwith format ;;
val failwith : ('a, unit, string, 'b) format4 -> 'a = <fun>
# failwith "hello, %s" "world" ;;
Exception: Failure "hello, world".
# failwith "error code %d" 3 ;;
Exception: Failure "error code 3".
ksprintf
is the key function here. Like sprintf
it takes a format string and a variable number of parameters, and makes a fixed result string. Unlike sprintf
it doesn't return the string, but passes it to the function which is its first parameter — in this case, the standard failwith
function. So ksprintf
is useful because it can turn almost any fixed string function into a printf-like function.Now how about the lesson on type arcana? Well if you know anything about currying you might think that you could write the new
failwith
function even shorter, like this:let failwith = ksprintf failwithIf you try this, you'll find the new function works some of the time, but fails to type-check at other times. In fact, the first time you use it, it seems to "remember" the type of all the arguments, and then refuses to work if any of those types change:
# failwith "hello, %s" "world" ;;If we take a close look at the inferred types of the wrong definition, we can see why:
Exception: Failure "hello, world".
# failwith "error code %d" 3 ;;
This expression has type (int -> 'a, 'b, 'c, 'd, 'd, 'a) format6
but is here used with type
(string -> 'e, unit, string, 'e) format4 =
(string -> 'e, unit, string, string, string, 'e) format6
# let failwith = ksprintf failwith ;;
val failwith : ('_a, unit, string, '_b) format4 -> '_a = <fun>
'_a
(with an underscore) is not a polymorphic type, but a single type that the compiler just hasn't been able to infer fully yet. As soon as you give it more information (eg. calling the function), the compiler infers that type into some concrete type (like string -> ...
above) and won't let you change it later.A more advanced question is to work out why type inference fails to infer the more general polymorphic type. I suspect this FAQ may have the answer.
3 comments:
Nice tip. Just thought I'd mention that the 'k' in ksprintf comes from continuation, and this is a nice example of continuation-passing style in practice.
Wow, thanks so much, I was wanting to do this for awhile. I was using Exceptions since they can be used with types.
This seems working:
let failwithf a = Printf.ksprintf failwith a;;
# failwithf "%s = %d" "a" 2;;
Exception: Failure "a = 2".
# failwithf "%d " 2;;
Exception: Failure "2 ".
#
Post a Comment