In F#, the compiler is clearly doing some magic to make this work:
printfn "%i %i" 6 7 ;; // good
printfn "%i %i" 6 7 8;; // error
How is it doing this? and is there any way to achieve a similar behavior, from within the language?
In F#, the compiler is clearly doing some magic to make this work:
printfn "%i %i" 6 7 ;; // good
printfn "%i %i" 6 7 8;; // error
How is it doing this? and is there any way to achieve a similar behavior, from within the language?
Sadly, this bit of "magic" (as you call it) is hard-coded into the F# compiler. You can extend the compiler, but the result will be non-standard F#.
Here is the specific bit of code that handles that (it isn't too readable, but that's how the F# compiler is written):
and TcConstStringExpr cenv overallTy env m tpenv s =
if (AddCxTypeEqualsTypeUndoIfFailed env.DisplayEnv cenv.css m overallTy cenv.g.string_ty) then
mkString cenv.g m s,tpenv
else
let aty = NewInferenceType ()
let bty = NewInferenceType ()
let cty = NewInferenceType ()
let dty = NewInferenceType ()
let ety = NewInferenceType ()
let ty' = mkPrintfFormatTy cenv.g aty bty cty dty ety
if (not (isObjTy cenv.g overallTy) && AddCxTypeMustSubsumeTypeUndoIfFailed env.DisplayEnv cenv.css m overallTy ty') then
// Parse the format string to work out the phantom types
let aty',ety' = (try Formats.ParseFormatString m cenv.g s bty cty dty with Failure s -> error (Error(FSComp.SR.tcUnableToParseFormatString(s),m)))
UnifyTypes cenv env m aty aty';
UnifyTypes cenv env m ety ety';
mkCallNewFormat cenv.g m aty bty cty dty ety (mkString cenv.g m s),tpenv
else
UnifyTypes cenv env m overallTy cenv.g.string_ty;
mkString cenv.g m s,tpenv
And here is the same code, that also supports numeric strings (i.e. printfn "%i %i" ("4" + 2) "5"
will type-check and print 6 5
):
and TcConstStringExpr cenv overallTy env m tpenv s =
if (AddCxTypeEqualsTypeUndoIfFailed env.DisplayEnv cenv.css m overallTy cenv.g.string_ty) then
mkString cenv.g m s,tpenv
elif (AddCxTypeEqualsTypeUndoIfFailed env.DisplayEnv cenv.css m overallTy cenv.g.int_ty) then
mkInt cenv.g m (System.Int32.Parse s),tpenv
elif (AddCxTypeEqualsTypeUndoIfFailed env.DisplayEnv cenv.css m overallTy cenv.g.int32_ty) then
mkInt32 cenv.g m (System.Int32.Parse s),tpenv
else
let aty = NewInferenceType ()
let bty = NewInferenceType ()
let cty = NewInferenceType ()
let dty = NewInferenceType ()
let ety = NewInferenceType ()
let ty' = mkPrintfFormatTy cenv.g aty bty cty dty ety
if (not (isObjTy cenv.g overallTy) && AddCxTypeMustSubsumeTypeUndoIfFailed env.DisplayEnv cenv.css m overallTy ty') then
// Parse the format string to work out the phantom types
let aty',ety' = (try Formats.ParseFormatString m cenv.g s bty cty dty with Failure s -> error (Error(FSComp.SR.tcUnableToParseFormatString(s),m)))
UnifyTypes cenv env m aty aty';
UnifyTypes cenv env m ety ety';
mkCallNewFormat cenv.g m aty bty cty dty ety (mkString cenv.g m s),tpenv
else
UnifyTypes cenv env m overallTy cenv.g.string_ty;
mkString cenv.g m s,tpenv
P.S.: I wrote this a long time ago, so I can't remember why there are both mkInt
and mkInt32
there. It might be necessary, and might not - but I do remember that this code worked.
The magic is in the implicit conversion from a string literal to the type PrintfFormat<_,_,_,_>
. For instance, printf
takes an argument of type TextWriterFormat<'a>
, which is actually just an alias for PrintfFormat<'a,System.IO.TextWriter,unit,unit>
.
The magic for this implicit conversion can't be easily emulated within the language, but there's nothing special about the printf
family of functions - you can write your own functions that take arguments of type PrintfFormat<_,_,_,_>
and use them with string literals without any problem.
While there's no way to extend the implicit conversion from strings, one alternative would be to use type providers. It would be quite easy to write a type provider so that
PrintfTypeProvider<"%i %i">.Apply
returns a value of type int -> int -> string
, for instance, but you could also extend the logic in fairly arbitrary ways if you wanted.
If you just want this behavior on strings without them being output, then sprintf
is a similar function that returns the string rather than printing it out. So you can have functions that return strings that are formatted with type-checking this way.