下面的代码显示了由两个相同签名(RwA -> RwA
)的函数处理某些记录的情况。然而,根据该变更器函数的实现,传递给它的数据可能被更改或不更改。
在一个更大的项目中,这样的事情是昂贵的。可能在维护或优化期间,函数的实现发生了更改,或者添加了更多的更改函数,程序看起来仍然相同,但突然以意想不到的方式运行。
因此,问题是:在这个示例中是否有任何方法来呈现代码,很明显(并检查了编译器),类型为RwAChanger
的函数可以也可能不会更改作为参数传递的数据?
请不要回答“不要使用数组”,因为这可能是大量的数据,例如三维网格的顶点或类似的,其中的性能将是一个问题。此外,我相信还可以找到其他示例,这些示例不涉及产生相同类型问题的数组。
// A record with arrays...
type RwA =
{
A : int array
I : int array
}
let x = [| for i in 0..10 -> i |]
let init a =
{ A = a; I = Array.map (fun v -> -v ) a }
// This function creates a new array as member A in RwA
let change (o : RwA) : RwA =
{ o with A = Array.map (fun v -> v + 42) o.A }
// This function modifies the value of the array in member A of an RwA instance.
let change2 (o: RwA) : RwA =
o.A.[0] <- 666
o
let dump (o : RwA) =
printfn "{ A = %A; I = %A; }" o.A o.I
o
let dumpA (a : int array) : int array =
printfn "x = %A" x
a
// Is there a way to express a contract about immutability?
type RwAChanger = RwA -> RwA
let transmogrify (changer : RwAChanger) a =
a
|> dumpA
|> init
|> dump
|> changer
|> dump
|> ignore
let test() =
transmogrify change x
dumpA x |> ignore
transmogrify change2 x
dumpA x |> ignore
do test()
发布于 2014-09-07 15:47:36
因此,问题是:在这个示例中是否有任何方法来呈现代码,很明显(并检查了编译器),类型为RwAChanger的函数可以也可能不会更改作为参数传递的数据?
答案基本上是肯定的;通常可以对属性进行编码,例如一个对象是否可以使用类型被某个特定的函数更改。最基本的技术是使用抽象类型。您定义了一组类型,一种类型对应于您希望区分的每种不同类型的访问,以及这些类型上的相关操作。
例如,在这种情况下,可以定义两种类型。首先,定义一个与只读视图相对应的类型:
// Read-only objects
module RA =
type RA
val change: RA -> RA
// and other operations on read-only objects
在上面的签名中,重要的是RA
类型是左抽象的。只有在签名中定义的操作才能用于操作RA
类型的对象。
然后,您还将定义另一个与读写视图相对应的类型:
// Read-write objects
module RwA =
type RwA
val change: RwA -> RwA
// and other operations on read-write objets
此签名还保留RwA
抽象类型。此外,还允许将RwA
对象视为RA
对象:
val readonly: RwA -> RA
反之亦然!
现在,只使用读操作的函数将被赋予接受RA
类型对象的类型,而使用写操作的函数将被赋予接受RwA
对象的类型。
上述方案的一个不便之处在于,为了对读-写对象使用只读操作,需要使用readonly
操作显式地将读-写对象转换为只读对象:
RwA.readonly (x: RwA.RwA) |> RA.change
有一个叫做幻影类型的技巧,它通常可以消除这种显式转换的需要。不是定义多个抽象类型,而是向抽象类型中添加一个或多个额外的类型参数来编码所需的属性。
作为幻影类型的一个例子,让我们为读-写数组定义一个类型,RWArray<'rw, 't>
,它允许一个人区分一个操作是否可以修改数组的元素。首先,这是签名:
// RWArray.fsi
type R
type W
type RWArray<'rw, 't>
module RWArray =
val zeroCreate: int -> RWArray<W, 't>
val readonly: RWArray<_, 't> -> RWArray<R, 't>
val length: RWArray<_, 't> -> int
val get: RWArray<_, 't> -> int -> 't
val set: RWArray<W, 't> -> int -> 't -> unit
RWArray<'rw, 't>
类型具有类型参数'rw
,用于编码一个对象是否可以修改。在这种情况下,只有一个变异操作,即set
操作,它要求类型参数可以与W
统一。其他操作允许类型参数为任意类型。换句话说,对于幻影类型参数'rw
,只读操作是多态的.
花点时间找出(或尝试使用编译器)下列函数定义的类型:
let modify xs x2x =
for i=0 to RWArray.length xs - 1 do
RWArray.set xs i << x2x <| RWArray.get xs i
let map xs x2y =
let ys = RWArray.zeroCreate (RWArray.length xs)
for i=0 to RWArray.length xs - 1 do
RWArray.set ys i << x2y <| RWArray.get xs i
ys
如您所见,modify
操作的类型需要一个可写数组,而map
操作不需要。现在,在定义高阶操作时,可以约束作为参数的函数,使其不允许数组发生变异。下面是一个示例:
let sillyExample (effect: RWArray<R, int> -> unit) : unit =
let rwa = RWArray.zeroCreate 1
RWArray.set rwa 0 31
effect (RWArray.readonly rwa)
RWArray.set rwa 0 (RWArray.get rwa 0 + 10)
effect (RWArray.readonly rwa)
在上面的函数定义中知道的是,对effect
的调用不能修改作为参数给出的数组(嗯,不使用反射)。注意,在上面,需要使用readonly
操作将数组交给effect
函数,但是对RWArray.get
的调用不需要它。
可以避免使用readonly
,方法是为effect操作使用另一种类型,该类型将要求该效果相对于'rw
参数具有多态性:
type Effect =
abstract Invoke: RWArray<'rw, int> -> unit
let sillyExample2 (effect: Effect) : unit =
let rwa = RWArray.zeroCreate 1
RWArray.set rwa 0 31
effect.Invoke rwa
RWArray.set rwa 0 (RWArray.get rwa 0 + 10)
effect.Invoke rwa
上述变化与以前的变化本质上具有相同的性质。换句话说,众所周知,对effect.Invoke
的调用不能修改数组。
RWArray
模块和相关类型的实现非常简单:
// RWArray.fs
type R = | R
type W = | W
type RWArray<'rw, 't> = {RWArray: array<'t>}
module RWArray =
let zeroCreate n : RWArray<W, 't> = {RWArray = Array.zeroCreate n}
let readonly (rwa: RWArray<_, _>) : RWArray<R, _> = {RWArray = rwa.RWArray}
let length rwa = rwa.RWArray.Length
let get rwa i = rwa.RWArray.[i]
let set (rwa: RWArray<W, _>) i x = rwa.RWArray.[i] <- x
术语“幻影类型”背后的原因是类型参数'rw
只显示为类型参数。如您所见,它不在RWArray<'rw, 't>
类型定义的右侧使用。
这种方法的实际实现可能会定义更多的操作。
有许多有趣的论文描述了使用ML样式类型系统编码属性的技术。我只提到一篇论文:没有更长的外国:教一个ML编译器说C“本地”。。我还要提到我几年前写的一篇文章:编码任意有限关系的幻影布尔函数。
这里有一个练习:设计,这是读写数组的一个更精化的版本,可以指定可以读取数组长度和
发布于 2014-09-05 15:48:41
我认为没有办法告诉编译器,现有的可变类型应该是不可变的,也不能像@mydogisbox在注释中提到的那样使用Contracts。
但是,您可以从ImmutableArray
记录中的Microsoft.Bcl.Immutable
包中使用RwA
。数组访问的性能差异应该是最小的。
发布于 2014-09-06 09:03:34
如果希望编译器错误区分更改的值和未更改的值,这很大程度上意味着不能对这两种函数使用相同的签名。
所以你不能使用:
type RwAChanger = RwA -> RwA
但你可以选择这样的东西:
type RwAChanger = RwA -> Changed RwA
type RwAUnchanger = RwA -> Unchanged RwA
它能准确地指示函数所做的事情。用你的例子:
type Changed<'T> = Changed of 'T
type Unchanged<'T> = Unchanged of 'T
let change2 (o: RwA) : RwA =
o.A.[0] <- 666
Changed o
let dump (o : RwA) =
printfn "{ A = %A; I = %A; }" o.A o.I
Unchanged o
您还可以创建一些帮助程序来执行类型安全的map
和bind
。下面是一个小型库的示例:
module ChangeLib =
type Changed<'T> = Changed of 'T
type Unchanged<'T> = Unchanged of 'T
type Changer<'T> = 'T -> Changed<'T>
type Unchanger<'T> = 'T -> Unchanged<'T>
let mapC f (Changed v) = Changed (f v)
let mapU f (Unchanged v) = Unchanged (f v)
/// if input is Unchanged and f returns a Changed,
/// the whole expression is Changed
let bindUC (f:'a Changer) (Unchanged v) = f v
/// if input is Unchanged and f returns a Unchanged,
/// the whole expression is Unchanged
let bindUU (f:'a Unchanger) (Unchanged v) = f v
/// if input is Changed and f returns a Unchanged,
/// the whole expression is Changed
let bindCU (f:'a Unchanger) (Changed v) =
let (Unchanged u) = f v
Changed u
/// if input is Changed and f returns a Changed,
/// the whole expression is Changed
let bindCC (f:'a Changer) (Changed v) = f v
下面是一个使用该库的示例:
module Example =
open ChangeLib
let change (o: int[]) =
o.[0] <- 42
Changed o
let copy (o: int[]) =
let o' = o |> Array.map (fun i -> i + 1)
Unchanged o'
let dump (o: int[])=
printfn "%A" o
Unchanged o
// has signature "int[] -> Changed int[]"
let transmogrifyWithChanges x =
x
|> change
|> bindCU dump
// has signature "int[] -> Unchanged int[]"
let transmogrifyWithoutChanges x =
x
|> copy
|> bindUU dump
// mixing and matching will preserve state
// so this function returns a Changed
// and has signature "int[] -> Changed int[]"
let transmogrify x =
x
|> transmogrifyWithoutChanges
|> bindUC transmogrifyWithChanges
// do some tests
let a = [| 1;2;3 |]
let changedA =
a |> transmogrifyWithChanges
let unchangedA =
a |> transmogrifyWithoutChanges
let changedA2 =
a |> transmogrify
是的,必须使用不同类型的bind
函数是很尴尬的,但是如果您想要编译器错误,这就是您所付出的代价!
使用重载或内联可能有一些技巧,可以让所有绑定函数都具有相同的名称,但就我个人而言,我喜欢让函数名指示类型的明确性。
至于性能,我不认为包装数组会造成很大的损失。
https://stackoverflow.com/questions/25688825
复制相似问题