【问题标题】:What is the best way to pass generic function that resolves to multiple types传递解析为多种类型的通用函数的最佳方法是什么
【发布时间】:2017-07-24 17:34:21
【问题描述】:

作为背景:它是功能 DI 的一种变体。在Scott's post 之后,我写了一个解释器。不同之处在于,我的解释器是通用的,并且根据您输入的内容进行了参数化。

出于测试目的,我想传递另一个解释器,问题就在这里 - 我该怎么做?以下是问题的简化大纲:

let y f =
    let a = f 1
    let b = f 2L
    (a,b)

f 是我的通用解释器,但这里显然受限于第一次使用int -> 'a。 在这个简化的场景中,我可以只传递解释器两次,但在我的实际实现中,类型空间相当大(基本类型 x3 输出类型)。

是否有一些 F# 机制可以让我做到这一点,而无需太多开销?

【问题讨论】:

  • 您可以将类型与单个方法一起使用,类似于委托但静态的。如果需要,我可以告诉你如何。另一种选择是使用@fyodorsoikin 的答案中显示的接口
  • 您不能使用静态类型和/或方法。无法将其作为参数传递。
  • 是的,你可以。我将发布一个示例来说明如何。
  • 请做。我很好奇。
  • @FyodorSoikin 尽管此问题已标记为已回答,但我应您的要求将其添加为另一个答案,因为此评论中没有足够的空间向您展示代码。

标签: f# functional-programming


【解决方案1】:

您不能在 F# 中使用函数执行此操作。函数作为值传递时会失去通用性。

不过,F# 确实有一种机制可以做到这一点,尽管有点笨拙:接口。接口方法可以是泛型的,因此您可以使用它们来包装泛型函数:

type Wrapper =
    abstract member f<'a> : 'a -> 'a

let y (w: Wrapper) = 
   let a = w.f 1 
   let b = w.f 2L 
   (a, b)

let genericFn x = x

// Calling y:
y { new Wrapper with member __.f x = genericFn x }

缺点是,你不能回到高阶函数,以免失去通用性。你必须有一直到海龟的接口。例如,您不能通过将其抽象为函数来简化实例创建:

let mkWrapper f = 
   // no can do: `f` will be constrained to a non-generic type at this point
   { new Wrapper with member __.f x = f x }

但另一方面,您可以提供一些便利。至少摆脱类型注释:

type Wrapper = abstract member f<'a> (x: 'a): 'a

let callF (w: Wrapper) x = w.f x

let y w = 
   let a = callF w 1 
   let b = callF w 2L 
   (a,b)

(注意:以上代码中可能存在一些小的语法错误,因为我是在手机上写的)

【讨论】:

    【解决方案2】:

    不确定您是否仍然感兴趣,因为您已经接受了答案,但正如@Fyodorsoikin 要求的那样,这是“静态”方式,这一切都发生在编译时,因此没有运行时开销:

    let inline y f =
        let a = f $ 1
        let b = f $ 2L
        (a, b)
    
    type Double = Double with static member inline ($) (Double, x) = x + x
    type Triple = Triple with static member inline ($) (Triple, x) = x + x + x
    
    type ToList = ToList with static member        ($) (ToList, x) = [x]
    
    let res1 = y Double
    let res2 = y Triple
    let res3 = y ToList
    

    当我需要在任意结构上使用通用函数时,我会使用这种技术,我会使用单个方法“Invokable”来命名类型。

    更新

    要向函数添加参数,请将其添加到 DU,如下所示:

    type Print<'a> = Print of 'a with
        static member inline ($) (Print printer, x) = printer (string x)
    
    let stdout (x:string) = System.Console.WriteLine x
    let stderr (x:string) = System.Console.Error.WriteLine x
    
    let res4 = y (Print stdout)
    let res5 = y (Print stderr)
    

    这只是一个快速简单的示例代码,但这种方法可以改进:您可以使用方法名称而不是运算符,可以避免在声明中重复 DU,并且可以编写 Invokables。如果您对这些增强功能感兴趣,请告诉我。我之前在生产代码中使用过这种方法的改进,从未遇到任何问题。

    【讨论】:

    • 嘿,这是作弊!函数是内联的!这不会回答最初的问题,更不用说看到目标是“依赖注入”。
    • @FyodorSoikin 是的,那么你可以称我为作弊开发者,因为我每次需要内联函数时都会使用它们。
    • 我也使用它们。广泛。使用本身并不是作弊。作弊就是在这个特定问题的上下文中使用它。
    • @FyodorSoikin 为什么?代码编译。您可以稍后定义另一个函数用于测试目的,如问题中所述,它将起作用。如果你想严格地回答这个问题,答案是没有答案,因为不可能传递一个函数来获得多种结果类型。
    • @FyodorSoikin 我最近使用了这么多内联函数,以至于我每天晚上都祈祷我永远不会有一个未计算的运行时异常,因为文件名/lineno 很可能会完全失控,我永远不会能够找到它的来源。此外,我每晚都为 TC 和 HKT 以及更高等级的类型祈祷 ;-)
    【解决方案3】:

    请看Crates

    这里有一个快速的 sn-p 描述你想要完成的关键。我相信这个 sn-p 很有价值,因为它有助于教会我们如何通过使用数学语言正式推理使用 F# 和其他 ML 类型系统。换句话说,它不仅向您展示了如何工作,而且还教会了您为什么它起作用的深层原理。

    这里的问题是我们已经达到了 F# 中可直接表达的基本限制。因此,模拟全称量化的技巧是避免直接传递函数,而是将类型参数隐藏起来,这样调用者就不能将其固定为一个特定的值,但是怎么可能做到这一点?

    回想一下,F# 提供了对 .NET 对象系统的访问。如果我们创建自己的类(在面向对象的意义上)并在其上放置一个通用方法呢?我们可以创建我们可以传递的实例,从而携带我们的函数(以所述方法的形式)?

    // Encoding the function signature...
    // val id<'a> : 'a -> 'a
    // ...in terms of an interface with a single generic method
    type UniversalId = abstract member Eval<'a> : 'a -> 'a
    

    现在我们可以创建一个实现,我们可以在不固定类型参数的情况下传递它:

    // Here's the boilerplate I warned you about.
    // We're implementing the "UniversalId" interface
    // by providing the only reasonable implementation.
    // Note how 'a isn't visible in the type of id -
    // now it can't be locked down against our will!
    let id : UniversalId =
      { new UniversalId with
          member __.Eval<'a> (x : 'a) : 'a = x
      }
    

    现在我们有一种方法可以模拟一个普遍量化的函数。我们可以将id 作为一个值传递,并且在任何时候我们选择一个类型'a 来传递给它,就像我们传递任何值级参数一样。

    存在量化

    There exists a type x, such that…
    

    existential 是一个静态类型未知的值,要么是因为我们有意隐藏了已知的东西,要么是因为类型确实是在运行时选择的,例如由于反思。但是,在运行时,我们可以检查存在性以找到其中的类型和值。

    如果我们不知道存在量化类型中的具体类型,我们如何安全地对其执行操作?好吧,我们可以应用任何本身可以处理任何类型值的函数——即我们可以应用一个普遍量化的函数!

    换句话说,存在性可以用可用于操作它们的普遍性来描述。

    这种技术非常有用,它被用于数据类型通用编程库 TypeShape,它允许您废弃您的样板 .NET 反射代码,以及用于“打包存在数据类型”的 MBrace 和 FsPickler。请参阅 Erik Tsarpalis 的 slides on TypeShape 了解更多关于“在 .NET 中编码 安全 存在解包”和在 .NET 中编码 rank-2 类型。

    像 TypeShape 这样的反射辅助库直观地也应该涵盖大部分(如果不是全部)用例:依赖注入需要在底层实现服务定位,因此 TypeShape 可以被认为是用于构建依赖关系的“原始组合库”注入。请参阅以Arbitrary Type Shapes 开头的幻灯片:特别注意 Code Lens 数据类型:

    type Lens<'T,'F> =
    {
        Get : 'T -> 'F
        Set : 'T -> 'F -> 'T
    }
    

    最后,如果您想了解更多想法,请阅读 Don Stewart 的博士论文,Dynamic Extension of Typed Functional Languages

    我们提出了一种解决静态扩展问题的方法 类型擦除的类型化功能语言。提出的解决方案保留 静态检查的好处,包括类型安全、积极优化和组件的本机代码编译,同时允许 程序在运行时的可扩展性。

    我们的方法是基于一个静态的动态扩展框架 类型化设置,结合动态链接,运行时类型检查, 一流的模块和代码热交换。 我们展示了这个框架 足以在任何情况下允许广泛的动态扩展功能 具有类型擦除语义的静态类型函数式语言。

    独特的是,我们使用完整的编译时类型系统来执行运行时 动态组件的类型检查,并强调使用native 代码扩展以确保静态类型的性能优势 保留在动态环境中。我们还开发了以下概念 完全动态的软件架构,其中静态核心最少且 所有代码都是可热插拔的。该方法的好处包括可热插拔 通过嵌入式领域特定的代码和复杂的应用程序扩展 语言。

    以下是 Don 列出的一些粗粒度设计模式,供未来工程师遵循:

    1. 第 3.6.3 节:专业化模拟器方法。
      • 演示如何将程序专业化技术应用于聚合物化学的蒙特卡罗模拟。这种方法演示了如何“注入”专门的代码来解决所谓的“窥孔优化”。

    以及帮助构建“可扩展性之塔”的一般图表:

    【讨论】:

      【解决方案4】:

      你可以用一个完整的类型来做到这一点:

      type Function() =
          member x.DoF<'a> (v:'a) = v
      
      let y (f: Function) =
          let a = f.DoF 1
          let b = f.DoF 2L
          (a,b)
      
      y (Function())
      

      我不知道如何让它与 F# 中的一流函数一起工作

      【讨论】:

      • 但是这里的主要问题,不是不能使用第一类函数,而是你不能“切换”到另一个函数,所以将它作为参数传递是没有意义的.
      猜你喜欢
      • 1970-01-01
      • 2010-09-05
      • 1970-01-01
      • 2018-06-09
      • 2013-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多