Compile-time constraints for strings in F#, similar to Units of Measure - is it possible?

爱⌒轻易说出口 提交于 2019-11-28 10:55:23

A bit late (I'm sure there's a time format where there is only one bit different between February 23rd and November 30th), I believe these one-liners are compatible for your goal:

type string<[<Measure>] 'm> = string * int<'m>

type string<[<Measure>] 'm> = { Value : string }

type string<[<Measure>] 'm>(Value : string) = struct end

In theory it's possible to use 'units' to provide various kinds of compile-time checks on strings (is this string 'tainted' user input, or sanitized? is this filename relative or absolute? ...)

In practice, I've personally not found it to be too practical, as there are so many existing APIs that just use 'string' that you have to exercise a ton of care and manual conversions plumbing data from here to there.

I do think that 'strings' are a huge source of errors, and that type systems that deal with taintedness/canonicalization/etc on strings will be one of the next leaps in static typing for reducing errors, but I think that's like a 15-year horizon. I'd be interested in people trying an approach with F# UoM to see if they get any benefit, though!

The simplest solution to not being able to do

"hello"<unsafe_user_input>

would be to write a type which had some numeric type to wrap the string like

type mystring<'t>(s:string) =
    let dummyint = 1<'t>

Then you have a compile time check on your strings

It's hard to tell what you're trying to do. You said you "need some runtime constraints" but you're hoping to solve this with units of measure, which are strictly compile-time. I think the easy solution is to create SafeXXXString classes (where XXX is Sql, Xml, etc.) that validate their input.

type SafeSqlString(sql) =
  do
    //check `sql` for injection, etc.
    //raise exception if validation fails
  member __.Sql = sql

It gives you run-time, not compile-time, safety. But it's simple, self-documenting, and doesn't require reading the F# compiler source to make it work.

But, to answer your question, I don't see any way to do this with units of measure. As far as syntactic sugar goes, you might be able to encapsulate it in a monad, but I think it will make it more clunky, not less.

You can use discriminated unions:

type ValidatedString = ValidatedString of string
type SmellyString = SmellyString of string

let validate (SmellyString s) =
  if (* ... *) then Some(ValidatedString s) else None

You get a compile-time check, and adding two validated strings won't generate a validated string (which units of measure would allow).

If the added overhead of the reference types is too big, you can use structs instead.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!