refined icon indicating copy to clipboard operation
refined copied to clipboard

Feature request: Add runtime exception check instead of error "compile-time refinement only works with literals"

Open soronpo opened this issue 8 years ago • 13 comments

Hi, I'm posting this feature request after a small e-mail exchange with @fthomas

The code

for (i <-  -1 to 3)
  println(refineMV[Positive](i))

currently results in compile-time refinement only works with literals error.

I suggest that instead this will result in a runtime check, meaning four possible cases:

  1. Compile time check pass for literal (no runtime check required)

      println(refineMV[Positive](1))
    

    Prints outs: 1

  2. Compile time check fail for literal

      println(refineMV[Positive](0))
    
  3. Compile time check could not run, so running runtime check instead, which passed

    for (i <-  1 to 3)
      println(refineMV[Positive](i)) 
    

    Prints outs: 1 2 3

  4. Compile time check could not run, so running runtime check instead, which failed and throws exception

    for (i <-  -1 to 3)
      println(refineMV[Positive](i))  //Throws exception
    

Note: Instead of modifying refineMV you may wish to keep it as is, and add another flavor (something like refineMVRE [Macro Verify, Runtime Exception], or whatever acronym you wish). However, I don't think you should. Keep it simple with the least functions possible.

Thanks, Oron

soronpo avatar Jul 11 '16 20:07 soronpo

But the point of refineMV, and in fact, refined as a library is that it should guarantee at compile time that the values at play are guaranteed to follow the specified constraints. That is, they won't blow up possibly resulting in an exception.

For values that you get at runtime and you'd want to check if it follows the specified constraints, you'd want to use refineV instead which returns an Either. This returns either a Right if it safely passes the constraints, and Left if it fails. Again, no throwing of exceptions.

When working with a good scala library, you'd typically expect it not to throw exceptions. Rather, error cases, you'd expect it to be fully spec'd out on the types. Such as the case is in refineV returning Either.

jvliwanag avatar Jul 11 '16 22:07 jvliwanag

Think about a library with a function def Foo(num : Positive) : Positive (where Positiveis an alias for Int Refined Positive). As a user of such library it is very clear to me that Foo(1) will run correctly and return a value. But what if I want to run it with a loop?

for (i <- 1 to 3)
  Foo(i)   // error: compile-time refinement only works with literals error

So now I need to have separate FooNonLiteral() function, or skip the compile time check completely. To me, my suggestion makes more sense. Yes, it is not a good practice for a library to throw exception in general, but in its current condition refineMV is not usable in every case, which is a shame.

For my use case, I'm creating a DSL (domain specific language) with Scala (and hopefully refined). The earlier I catch user errors, the better. So I need refineMVfor compile time check, but if this forces me to not be able to use it for runtime non-literal variables, then refined has no use for me.

soronpo avatar Jul 12 '16 07:07 soronpo

my tl;dr:

  • This is an interesting idea because it is a safer variant of unsafeFrom.
  • We should not change the semantics of refineMV.
  • By default all functions in refined should be referentially transparent and total (not throw exceptions) unless their name contains unsafe.

The library already contains the unsafeFrom function that refines values if they satisfy the constraint or throw an exception. This check always happen at runtime, even if a compile-time constant is passed to it. With @soronpo's proposal unsafeFrom could be improved to check literals at compile-time. I think this could be done without loss of its generality.

I agree with @jvliwanag that the purpose of refined is to have safer code and by default it should not use exceptions if not explicitly documented or apparent from the function names. Changing the semantics of refineMV could do more harm than good. It could mask programmer errors that only unveil at runtime.

So I think a safer unsafeFrom could be good compromise between @soronpo's idea and @jvliwanag concerns. What do you think?

Btw, the usage of unsafeFrom currently looks like this:

scala> refineV[Positive].unsafeFrom(5)
res0: Refined[Int,Positive] = 5

scala> refineV[Positive].unsafeFrom(-5)
java.lang.IllegalArgumentException: Predicate failed: (-5 > 0).

fthomas avatar Jul 12 '16 14:07 fthomas

Sounds reasonable. Just be sure that if passed compiler check, then runtime check would not run.

soronpo avatar Jul 12 '16 21:07 soronpo

I started working on this here: https://github.com/fthomas/refined/compare/topic/unsafeFrom2

fthomas avatar Jul 14 '16 20:07 fthomas

@fthomas Any updates on this?

AndreasKostler avatar Mar 28 '17 23:03 AndreasKostler

@AndreasKostler Currently I've little time to work on this. But in principle I'm still okay with changing unsafeFrom as outlined in https://github.com/fthomas/refined/issues/188#issuecomment-232068707. The only downside of this approach I'm aware of, is that unsafeFrom can't be eta expanded (i.e. unsafeFrom _) since it would be a macro. Macros in general can't be eta expanded.

fthomas avatar Mar 29 '17 08:03 fthomas

Hi guys, i know you had this discussion, but it will be really cool to have a functionality that do a compile time check and falls back to runtime if compile time failed! Here is my example:

type BucketNamePredicate = And[MinSize[W.`3`.T], MaxSize[W.`37`.T]]
type BucketName = String Refined BucketNamePredicate

def createBucketT(bucketName: BucketName) ...

sometimes i do have bucketName as literal but sometimes i want to get it from config. And i want still to use Refined type in method signature, since it clearly describes the requirements to the buckeName.

Or do you think that there is a way to model this with the solution you proposing?

ysusuk avatar Jun 30 '17 09:06 ysusuk

@ysusuk I created a solution using the singleton-ops library, which is called TwoFace and Checked values. You can read about it in the following link: https://contributors.scala-lang.org/t/twoface-values-closing-the-gap-between-run-compile-time-functionality/869

soronpo avatar Jun 30 '17 11:06 soronpo

@ysusuk If we change unsafeFrom as outlined in https://github.com/fthomas/refined/issues/188#issuecomment-232068707, you could invoke createBucketT with createBucketT(unsafeFrom(x)) where x is of type String and it would perform a compile-time check if x is a literal and a runtime check if it is not. Would this work for you?

The only reason this is still not implemented is my sparse free time. If someone else want to pick this up, that would be great.

fthomas avatar Jun 30 '17 13:06 fthomas

yeah, that would indeed work for me. for the moment i did that workaround

type BucketNamePredicate = And[MinSize[W.`3`.T], MaxSize[W.`37`.T]]
type BucketName = String Refined BucketNamePredicate

implicit class StringOps(str: String) {
  def bucketName: BucketName =
    Refined.unsafeApply[String, BucketNamePredicate](str)

so i do have nice description of BucketName requirements, but only runtime checks then, which is a good tradeoff, if i'm reading values from configs.

So i will need just to use unsafeApply2 instead of unsafeApply

ysusuk avatar Jun 30 '17 14:06 ysusuk

@fthomas what help do you need with https://github.com/fthomas/refined/compare/topic/unsafeFrom2?

ysusuk avatar Jun 30 '17 14:06 ysusuk

 @fthomas i'm trying to make this one https://github.com/fthomas/refined/compare/topic/unsafeFrom2 work

ysusuk avatar Jul 03 '17 14:07 ysusuk