F# 判别联合类型问题

6

我遇到了一个问题,无法使我的DU按预期工作。我定义了一个新的DU,其中其结果类型为<'a>或者从System.Exception派生的任何异常。

open System

// New exceptions.
type MyException(msg : string) = inherit Exception(msg)
type MyOtherException(msg : string) = inherit MyException(msg)

// DU to store result or an exception.
type TryResult<'a, 't> =
    | Result of 'a
    | Error of 't :> Exception

//This is fine.
let result = Result "Test"

// This works, doing it in 2 steps
let ex = new MyOtherException("Some Error")
let result2 = Error ex

// This doesn't work. Gives "Value Restriction" error.
let result3 = Error (new MyOtherException("Some Error"))

我不明白为什么我可以用两步创建一个“错误”,但是当我在同一行上执行相同的操作时,却会出现值限制错误。
我错过了什么吗?
谢谢
更新:
查看@kvb发布的帖子后,每次需要创建一个错误时添加类型信息似乎有点啰嗦,所以我将其封装成了一个额外的方法来创建错误,并且更加简洁。
// New function to return a Result
let asResult res : TryResult<_,Exception> = Result res

// New function to return an Error
let asError (err : Exception) : TryResult<unit,_> = Error(err)

// This works (as before)
let myResult = Result 100

// This also is fine..
let myResult2 = asResult 100

// Using 'asError' now works and doesn't require any explicit type information here.
let myError = asError (new MyException("Some Error"))

我不确定使用“unit”指定错误是否会产生我尚未预见的任何后果。

TryResult<unit,_> = Error(err)
2个回答

5

考虑稍微变换一下:

type MyOtherException(msg : string) = 
    inherit MyException(msg)
    do printfn "%s" msg

let ex = new MyOtherException("Some Error") // clearly, side effect occurs here
let result2 = Error ex // no side effect here, but generalized value

let intResults =    [Result 1; result2]
let stringResults = [Result "one"; result2]  // can use result2 at either type, since it's a generalized value

let result3 = Error (MyOtherException("Some Error")) // result would be of type TryResult<'a, MyOtherException> for any 'a

// In some other module in a different compilation unit
let intResults2 =    [Result 1; result3]     // why would side effect happen here? just using a generic value...
let stringResults2 = [Result "one"; result3] // likewise here...

问题在于看起来result3是一个值,但.NET类型系统不支持泛型值,它只支持具体类型的值。因此,每次使用result3时都需要调用MyOtherException构造函数;然而,这将导致任何副作用发生多次,这是令人惊讶的。如Ringil所建议的那样,您可以通过告诉编译器无论如何将表达式视为值来解决此问题:
[<GeneralizableValue>]
let result3<'a> : TryResult<'a,_> = Error(new MyOtherException("Some Error"))

只要构造函数没有副作用,这种方法就是可行的。

谢谢。这很有道理。我更新了我的问题,添加了一个额外的错误创建方法,它似乎工作得很好,并使其更加整洁。唯一的问题是类型现在被指定为TryResult<unit,_>。不确定这是否会有任何不利影响。 - Martin Cooper

2
你可以做:
let result3<'a> = Error (new MyOtherException("Some Error"))

编辑:

至于为什么您不能一步完成它,首先请注意这会导致相同的错误:

let result4 = Result (new MyOtherException("Some Error"))

就像这个一样:
let result4 = Result ([|1;|])

但是这个有效:
let result4 = Result ([1;])

什么是Exception和Arrays的相似之处,但不是Lists?这是它们的可变性。当您尝试使用单个步骤中可变类型创建TryResult时,值限制将会困扰您。
现在关于为什么两个步骤的过程可以解决这个问题,因为构造函数使整个函数不可泛化,因为您正在将一个函数应用于构造函数。但将其拆分成两个步骤就可以解决这个问题了。这类似于MSDN上的Case 2。
您可以在上述MSDN文章以及更深入的博客文章(此处)中了解更多信息。

好的,谢谢。这是一个解决方法。你有没有想过为什么在两个步骤中完成它不需要这个额外的参数? - Martin Cooper
我添加了一些解释。 - Ringil
4
数组可变而列表不可变并非关键,关键在于什么被视为“可泛化表达式”。例如,尝试使用 Result([1] @ [2])Result [| |] 会发生什么。另外,你的解决方法对我来说并不好;生成的类型签名是 val result3<'a> : TryResult<obj, MyOtherException> ,不够通用;如果你采用这种方法,应该添加类型注释。 - kvb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接