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?
可悲的是,这一点“魔法”(正如您所说的那样)被硬编码到 F# 编译器中。您可以扩展编译器,但结果将是非标准的 F#。
这是处理该问题的特定代码(它不太可读,但这就是 F# 编译器的编写方式):
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
这是相同的代码,它也支持数字字符串(即printfn "%i %i" ("4" + 2) "5"
类型检查和打印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
PS:我很久以前写的,所以我不记得为什么两者mkInt
都有mkInt32
。这可能是必要的,也可能不是 - 但我确实记得这段代码有效。
神奇之处在于从字符串文字到类型的隐式转换PrintfFormat<_,_,_,_>
。例如,printf
接受一个类型的参数TextWriterFormat<'a>
,它实际上只是一个别名PrintfFormat<'a,System.IO.TextWriter,unit,unit>
。
这种隐式转换的魔力无法在语言中轻松模拟,但printf
函数系列并没有什么特别之处——您可以编写自己的函数,接受类型参数PrintfFormat<_,_,_,_>
并将它们与字符串文字一起使用,没有任何问题。
虽然无法从字符串扩展隐式转换,但一种替代方法是使用类型提供程序。编写一个类型提供程序会很容易,这样
PrintfTypeProvider<"%i %i">.Apply
例如,返回一个 type 的值int -> int -> string
,但如果你愿意,你也可以以相当任意的方式扩展逻辑。
如果您只想在不输出字符串的情况下对字符串进行此行为,那么sprintf
是一个类似的函数,它返回字符串而不是将其打印出来。因此,您可以拥有返回以这种方式进行类型检查格式化的字符串的函数。