【问题标题】:Cannot create apply function with static language?无法使用静态语言创建应用函数?
【发布时间】:2011-04-11 05:49:48
【问题描述】:

我已经读过,使用 Scala 或 Haskell 等静态类型语言无法创建或提供 Lisp apply 函数:

(apply #'+ (list 1 2 3)) => 6

或许

(apply #'list '(list :foo 1 2 "bar")) => (:FOO 1 2 "bar")
(apply #'nth (list 1 '(1 2 3))) => 2

这是真的吗?

【问题讨论】:

  • C# 是静态的,并且有一个名为 Invoke 的函数,类似于 apply
  • IIRC,C# 使用动态来做到这一点,这意味着它会生成胶水代码,在传递它们之前检查所有 I/O 类型。
  • @sreservoir:在一般情况下,静态类型检查等同于解决停机问题,因此无法确定。这意味着存在类型安全但不可类型检查的程序。换句话说:任何静态类型的语言都会阻止您编写某些完全类型安全的程序。即使是最铁杆的静态打字爱好者,这也基本上是无可争议的。然而,在这个类是否包含任何有用的程序方面,静态类型和动态类型的支持者之间存在分歧。这基本上是在问apply是不是这样的程序。
  • 特别是,applyeval(尤其是特殊的eval)是动态类型的支持者经常声称不可能在静态类型语言中实现的一些程序。至少在实际存在的那些中。例如,如果你在Data.Dynamic 模块中查看Haskell 的dynApply 的源代码,这可能是最接近Scheme 的apply 的东西,你会发现它使用了unsafeCoerce 函数,这基本上是与 C 中不受限制的不安全强制转换相同,因此显式且有意地规避了类型系统。
  • @Jörg W Mittag:作为“硬核静态类型爱好者”(顺便说一句,C# 和 Java 不算真正的静态类型语言),我非常同意。有一些变通方法,但基本上没有办法在不以某种方式规避类型系统的情况下制作一个合适的一流eval。事实上,如果你堵住所有的漏洞,并把静态类型归结为它的逻辑结论,那么根本就不可能为一种语言本身编写一个解释器。在迁移到 Haskell 之前使用过 Scheme 和 Ruby,我有时确实会想念这种事情......

标签: scala programming-languages haskell lisp static-typing


【解决方案1】:

在静态类型语言中是完全可能的。整个java.lang.reflect 的事情就是这样做的。当然,使用反射可以为您提供与使用 Lisp 一样多的类型安全性。另一方面,虽然我不知道是否有支持这种特性的静态类型语言,但在我看来是可以做到的。

让我展示一下我认为如何扩展 Scala 以支持它。首先,让我们看一个更简单的例子:

def apply[T, R](f: (T*) => R)(args: T*) = f(args: _*)

这是真正的 Scala 代码,它可以工作,但它不适用于任何接收任意类型的函数。一方面,符号T* 将返回一个Seq[T],这是一个本地类型的序列。但是,也有异构类型的序列,例如HList

所以,首先,让我们在这里尝试使用HList

def apply[T <: HList, R](f: (T) => R)(args: T) = f(args)

Scala 仍然可以使用,但我们对f 施加了很大的限制,说它必须接收HList,而不是任意数量的参数。假设我们使用@ 将异构参数转换为HList,同样的方法* 从同类参数转换为Seq

def apply[T, R](f: (T@) => R)(args: T@) = f(args: _@)

我们不再谈论现实生活中的 Scala,而是对其进行假设性改进。这在我看来是合理的,除了 T 按类型参数表示法应该是一种类型。也许我们可以用同样的方式扩展它:

def apply[T@, R](f: (T@) => R)(args: T@) = f(args: _@)

在我看来,这似乎可行,尽管这可能是我的幼稚。

让我们考虑另一种解决方案,一种取决于参数列表和元组的统一。假设 Scala 终于统一了参数列表和元组,并且所有元组都是抽象类Tuple 的子类。然后我们可以这样写:

def apply[T <: Tuple, R](f: (T) => R)(args: T) = f(args)

那里。做一个抽象类Tuple是小菜一碟,元组/参数列表统一也不是牵强附会。

【讨论】:

  • 使用任何形式的反射都是在欺骗解决方案,因为它“为您提供与 Lisp 一样多的类型安全性”。一个合适的解决方案是typed apply——很像$接近funcall的静态类型版本。
  • @Eli 只有第一段谈到了反射的解决方案。其余的都是纯静态类型。
  • Daniel:确实,你不是在谈论反射;但是你想象中的“假设我们使用@来进行从异构参数到HList的转换”正是这里的问题!如果没有语言的一些严格支持,您不能只是从一个类型切换到一个类型序列:hlist 仍然是该语言中的一个值,并且没有apply,函数参数序列不是该语言中的东西。这是您无法直接访问的二等概念。至于它是否可以工作的问题,当然可以——我指的是Typed Racket...(这很难。)
  • (我应该补充一点:是的,这是可能的,只是几乎所有静态类型语言(包括 Scala)都没有这样做——Typed Racket 是一个明显的例外。)
【解决方案2】:

完整的应用在静态语言中是很困难的。

在 Lisp 中 APPLY 将函数应用于参数列表。函数和参数列表都是 APPLY 的参数。

  • APPLY 可以使用任何功能。这意味着这可以是任何结果类型和任何参数类型。

  • APPLY 采用任意长度的任意参数(在 Common Lisp 中,长度受实现特定常量值的限制),具有任意且可能不同的类型。

  • APPLY 返回由作为参数的函数返回的任何类型的值。

一种类型如何在不破坏静态类型系统的情况下进行检查?

例子:

(apply #'+ '(1 1.4))   ; the result is a float.

(apply #'open (list "/tmp/foo" :direction :input))
; the result is an I/O stream

(apply #'open (list name :direction direction))
; the result is also an I/O stream

(apply some-function some-arguments)
; the result is whatever the function bound to some-function returns

(apply (read) (read))
; neither the actual function nor the arguments are known before runtime.
; READ can return anything

交互示例:

CL-USER 49 > (apply (READ) (READ))                        ; call APPLY
open                                                      ; enter the symbol OPEN
("/tmp/foo" :direction :input :if-does-not-exist :create) ; enter a list
#<STREAM::LATIN-1-FILE-STREAM /tmp/foo>                   ; the result

现在以函数 REMOVE 为例。我们将从不同事物的列表中删除字符 a。

CL-USER 50 > (apply (READ) (READ))
remove
(#\a (1 "a" #\a 12.3 :foo))
(1 "a" 12.3 :FOO)

请注意,您也可以应用 apply 本身,因为 apply 是一个函数。

CL-USER 56 > (apply #'apply '(+ (1 2 3)))
6

还有一点复杂,因为函数 APPLY 接受任意数量的参数,其中只有最后一个参数需要是一个列表:

CL-USER 57 > (apply #'open
                    "/tmp/foo1"
                    :direction
                    :input
                    '(:if-does-not-exist :create))
#<STREAM::LATIN-1-FILE-STREAM /tmp/foo1>

如何处理?

  • 放松静态类型检查规则

  • 限制应用

以上一项或两项都必须使用典型的静态类型检查编程语言来完成。两者都不会给你一个完全静态检查和完全灵活的应用。

【讨论】:

  • @FUZxxl:怎么样,你的帖子充满了限制,“不自动推断类型”放宽了静态类型。
  • @Eli Barzilay,APPLY 的一个典型用例是将其与可变长度参数列表和不同类型的参数一起使用。如果这样的用例不能在静态类型语言中使用 APPLY 复制,那么它不是 Lisp 提供的 APPLY。 OP 询问了 Lisp 应用功能。如果将 APPLY 限制在静态类型语言允许的范围内,那么它仅涵盖 APPLY 的简单用途,而不是 OP 所询问的 Lisp 应用。
  • 在这行之后,您将答案简化为微不足道的“否”,原因与apply 无关,因此您完全忽略了一个指向 真正的流行类型系统的缺陷。
  • @Eli Barzilay,原因与 APPLY 不无关系。 APPLY 用于 Lisp 中的某些类型的用例。我描述了一些功能,以确保 OP 理解 APPLY 不是 REDUCE 的情况,而是在 Lisp 中用于更多用途。 APPLY 的这些用例很难在静态类型语言中复制,这不是我的问题。
  • @Eli Barzilay。根据您的回答:具有“有限类型”的“统一”列表或具有静态已知长度的“列表”。听起来不像 Lisp 的 APPLY,它没有这些限制。将 APPLY 与统一列表一起使用对我来说几乎毫无用处,尤其是因为可以通过 REDUCE 提供主要用例。将 APPLY 与静态长度的列表一起使用听起来也很奇怪。
【解决方案3】:

在大多数静态类型语言中你不能这样做的原因是它们几乎都选择了一个仅限于统一列表的列表类型。 Typed Racket 是一个可以讨论非统一类型列表的语言的示例(例如,它有一个 Listof 用于统一列表,List 用于具有静态已知长度的列表,可以是非统一的) -- 但它仍然为 Racket 的apply 分配了一个有限的类型(带有统一列表),因为真正的类型非常难以编码。

【讨论】:

  • 在 Scala 和 Haskell 中,这些被称为元组。它们对它们的长度和每个元素的类型进行编码。
  • 除了Tuples,还有HLists(异构列表)。像元组一样,它们对参数的长度和类型进行编码。与元组不同,它们不需要每个元组都使用新的类型构造函数,并且支持类型连接等操作。比元组更难使用,但它们确实展示了现代类型编程的可能性。 homepages.cwi.nl/~ralf/HList
  • @MJP 元组不是一回事。 HList 会更像它——在每个成员处完全键入的任意长度数据结构。顺便说一句,Scala 和 Haskell 也存在这样的情况。
  • MJP:不,元组与我所说的并不完全一样,并且试图在 Haskell 中将它们具体化为参数列表可能会令人困惑,因为 Haskell 只有一元函数。我不知道任何关于 HLists 的具体信息,但它听起来更接近我所说的。
  • Eli Barzilay:HList 是一个用于处理包含任意类型值的列表的库。 HList 的“类型”也是一个列表,包含每个项目的类型。基本上相当于以 () 作为空列表的右嵌套 2 元组。深度魔法用于创建对任意 HList 起作用的函数,但要执行许多有用的操作,所有类型仍必须在编译时知道。
【解决方案4】:

这在 Scala 中是微不足道的:

Welcome to Scala version 2.8.0.final ...

scala> val li1 = List(1, 2, 3)
li1: List[Int] = List(1, 2, 3)

scala> li1.reduceLeft(_ + _)
res1: Int = 6

好的,无类型:

scala> def m1(args: Any*): Any = args.length
m1: (args: Any*)Any

scala> val f1 = m1 _
f1: (Any*) => Any = <function1>

scala> def apply(f: (Any*) => Any, args: Any*) = f(args: _*)
apply: (f: (Any*) => Any,args: Any*)Any

scala> apply(f1, "we", "don't", "need", "no", "stinkin'", "types")
res0: Any = 6

也许我混淆了funcallapply,所以:

scala> def funcall(f: (Any*) => Any, args: Any*) = f(args: _*)
funcall: (f: (Any*) => Any,args: Any*)Any

scala> def apply(f: (Any*) => Any, args: List[Any]) = f(args: _*)
apply: (f: (Any*) => Any,args: List[Any])Any

scala> apply(f1, List("we", "don't", "need", "no", "stinkin'", "types"))
res0: Any = 6

scala> funcall(f1, "we", "don't", "need", "no", "stinkin'", "types")
res1: Any = 6

【讨论】:

  • 不,这些reduce、fold 和friends 不是 apply
  • 我想我需要看看你想要什么的定义,而不仅仅是例子。但我认为几乎可以肯定的是,无论你想做什么,都可以在 Scala 中以合理的方式完成。
  • apply 在 CL 中的定义只是“这会将函数应用于参数列表。”,在 R5RS 中,它被定义为“使用列表中的元素调用 proc [...]作为实际的论点。(无论如何,我高度怀疑你的“近乎确定性”。)
  • @Randall Schulz:这并不公平——这不是关于“运行时的类型错误”,而是关于一流的元编程。这绝对是合理的、有用的、很棒的,并且在静态类型语言中真的很难做到。在一个理想的世界中,我不仅能够在编译时检查我的程序是否类型良好,而且我的元编程本身只会在运行时生成类型良好的程序。这是一个难题,但并非不可能!
  • @Randall 当然,如果该语言为类型列表提供编译时支持,这是可能的。然后我们可以这样写:def apply[@T, R](f: (@T =&gt; R), args: @T*): R = f(args: _*),其中@T 代表类型列表args 类似于HList
【解决方案5】:

可以用静态类型语言编写apply,只要函数以特定方式键入即可。在大多数语言中,函数具有单独的参数,要么通过拒绝(即没有可变参数调用)或类型化的接受(即可变参数调用可能,但仅当所有其他参数都是 T 类型时)终止。以下是您在 Scala 中建模的方法:

trait TypeList[T]
case object Reject extends TypeList[Reject]
case class Accept[T](xs: List[T]) extends TypeList[Accept[T]]
case class Cons[T, U](head: T, tail: U) extends TypeList[Cons[T, U]]

请注意,这不会强制格式正确(尽管我相信确实存在类型界限),但您明白了。然后你有apply这样定义:

apply[T, U]: (TypeList[T], (T => U)) => U

那么,您的函数是根据类型列表事物定义的:

def f (x: Int, y: Int): Int = x + y

变成:

def f (t: TypeList[Cons[Int, Cons[Int, Reject]]]): Int = t.head + t.tail.head

还有像这样的可变参数函数:

def sum (xs: Int*): Int = xs.foldLeft(0)(_ + _)

变成这样:

def sum (t: TypeList[Accept[Int]]): Int = t.xs.foldLeft(0)(_ + _)

所有这一切的唯一问题是,在 Scala(以及大多数其他静态语言)中,类型不足以定义任何 cons 样式结构和固定长度元组之间的同构。因为大多数静态语言不代表递归类型的函数,所以你没有灵活性来透明地做这样的事情。 (当然,宏会改变这一点,并首先鼓励对函数类型进行合理的表示。但是,使用apply 会对性能产生负面影响,原因很明显。)

【讨论】:

    【解决方案6】:

    在 Haskell 中,没有用于多类型列表的数据类型,尽管我相信您可以使用神秘的 Typeable 类型类一起破解类似的东西。如我所见,您正在寻找一个函数,该函数接受一个函数,该函数包含与函数所需数量完全相同的值并返回结果。

    对我来说,这对于 haskells uncurryfunction 来说看起来很熟悉,只是它需要一个元组而不是一个列表。不同之处在于,一个元组始终具有相同数量的元素(因此(1,2)(1,2,3) 属于不同类型(!))并且内容可以是任意类型的。

    uncurry 函数有这样的定义:

    uncurry :: (a -> b -> c) -> (a,b) -> c
    uncurry f (a,b) = f a b
    

    您需要的是某种 uncurry,它以某种方式重载以提供任意数量的参数。我想到了这样的事情:

    {-# LANGUAGE MultiParamTypeClasses #-}
    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE UndecidableInstances #-}
    
    class MyApply f t r where
      myApply :: f -> t -> r
    
    instance MyApply (a -> b -> c) (a,b) c where
      myApply f (a,b) = f a b
    
    instance MyApply (a -> b -> c -> d) (a,b,c) d where
      myApply f (a,b,c) = f a b c
    
    -- and so on
    

    但这只有在编译器知道所有涉及的类型时才有效。可悲的是,添加 fundep 会导致编译器拒绝编译。由于我不是haskell 大师,也许其他人知道如何解决这个问题。遗憾的是,我不知道如何更轻松地存档。

    简历: apply 在 Haskell 中不是很容易,尽管可能。我想,你永远不需要它。

    编辑我现在有了一个更好的主意,给我十分钟,我会向你介绍一些没有这些问题的东西。

    【讨论】:

    • FUZxxl:为此,您需要无限定义。
    • 是的。我看到了一些没有无限的定义,但我忘记了在哪里。让我去找。
    • 而且我也知道,这是重复的。
    • 我尝试了半个小时来让这个用户友好。这个想法是编写一个flatTuple :: (a, b...) -&gt; (a,(b,...)) 类型的通用flatTuple 函数,并将元素递归地应用于函数。但是由于某种原因,类型系统对我的想法不满意 sniff
    【解决方案7】:

    尝试折叠。它们可能与您想要的相似。写一个特例就行了。

    haskell:foldr1 (+) [0..3] => 6

    顺便说一句,foldr1 在功能上等同于 foldr,累加器被初始化为列表的元素。

    有各种各样的褶皱。从技术上讲,他们都在做同样的事情,尽管方式不同,并且可能以不同的顺序进行论证。 foldr 只是比较简单的一种。

    【讨论】:

    • 顺便说一句,foldl 在大多数情况下是首选。
    • apply 函数仅调用该函数一次,并使用提供的列表作为其参数。 OP 的示例不会使用 add 函数折叠列表 1,2,3,它会使用列表 1,2,3 调用 add 函数一次,而 add 函数会自行执行循环。
    • 任何类型的折叠都与apply无关。
    • 当然,我刚刚意识到我完全错过了apply 的重点。
    • @Brian Campbell:你显然是对的;但是如果我们只关心这个具体的例子,那么在没有apply 的情况下,有很多方法可以实现它。例如 -- "6" 也可以正常工作,甚至是一个更短的程序。
    【解决方案8】:

    this page,我读到“Apply 就像 funcall,只是它的最后一个参数应该是一个列表;该列表的元素被视为是 funcall 的附加参数。”

    在 Scala 中,函数可以有 varargs(可变参数),就像 Java 的新版本一样。您可以使用符号:_* 将列表(或任何 Iterable 对象)转换为更多可变参数参数示例:

    //The asterisk after the type signifies variadic arguments
    def someFunctionWithVarargs(varargs: Int*) = //blah blah blah...
    
    val list = List(1, 2, 3, 4)
    someFunctionWithVarargs(list:_*)
    //equivalent to
    someFunctionWithVarargs(1, 2, 3, 4)
    

    事实上,即使是 Java 也可以做到这一点。 Java varargs 可以作为参数序列或数组传递。你所要做的就是将你的 Java List 转换为一个数组来做同样的事情。

    【讨论】:

    • apply的主要观点是正在应用的功能不需要改变。
    • 什么?我在哪里更改了函数?
    • 你已经用varargs: 定义了它。 OTOH,apply 可以与任何东西一起使用(甚至+,如问题所示)。
    • 相反,Lisp 的 + 是用可变参数定义的。在 Lisp 中,你可以说 (+ 1 2 3 4)。
    • MJP:没错,但这只是一个例子。 apply 可以与 any lisp 函数一起使用,包括那些不是可变参数的函数。 (我刚刚编辑并添加了这样一个示例。)
    【解决方案9】:

    静态语言的好处是它会阻止你将函数应用于不正确类型的参数,所以我认为它会更难做是很自然的。

    给定一个参数列表和一个函数,在 Scala 中,一个元组将最好地捕获数据,因为它可以存储不同类型的值。考虑到这一点,tupledapply 有一些相似之处:

    scala> val args = (1, "a")
    args: (Int, java.lang.String) = (1,a)
    
    scala> val f = (i:Int, s:String) => s + i
    f: (Int, String) => java.lang.String = <function2>
    
    scala> f.tupled(args)
    res0: java.lang.String = a1
    

    对于一个参数的函数,实际上有apply

    scala> val g = (i:Int) => i + 1
    g: (Int) => Int = <function1>
    
    scala> g.apply(2)
    res11: Int = 3
    

    我认为,如果您认为 apply 就像将一流函数应用于其参数的机制一样,那么这个概念在 Scala 中就存在。但我怀疑lisp中的apply更强大。

    【讨论】:

      【解决方案10】:

      对于 Haskell,要动态执行此操作,请参阅 Data.Dynamic,尤其是 dynApp:http://www.haskell.org/ghc/docs/6.12.1/html/libraries/base/Data-Dynamic.html

      【讨论】:

        【解决方案11】:

        请参阅他对 haskell 的动态内容,在 C 中,void 函数指针可以转换为其他类型,但您必须指定将其转换为的类型。 (我想,有一段时间没做函数指针了)

        【讨论】:

          【解决方案12】:

          Haskell 中的列表只能存储一种类型的值,所以你不能做像(apply substring ["Foo",2,3]) 这样有趣的事情。 Haskell 也没有可变参数函数,所以 (+) 只能接受两个参数。

          Haskell 中有一个 $ 函数:

          ($)                     :: (a -> b) -> a -> b
          f $ x                   =  f x
          

          但这真的很有用,因为它的优先级很低,或者绕过 HOF。

          我想你也许可以使用元组类型和fundeps来做这样的事情?

          class Apply f tt vt | f -> tt, f -> vt where
            apply :: f -> tt -> vt
          
          instance Apply (a -> r) a r where
            apply f t = f t
          
          instance Apply (a1 -> a2 -> r) (a1,a2) r where
            apply f (t1,t2) = f t1 t2
          
          instance Apply (a1 -> a2 -> a3 -> r) (a1,a2,a3) r where
            apply f (t1,t2,t3) = f t1 t2 t3
          

          我猜那是一种“uncurryN”,不是吗?

          编辑:这实际上并没有编译;被@FUZxxl 的回答所取代。

          【讨论】:

          • 没关系;那行不通,我猜是因为它们重叠了。所以我猜你需要做apply1 f t = f tapply2 f (t1,t2) = f t1 t2,等等......
          • 可变函数:是的,我们可以! stackoverflow.com/questions/3467279/…
          • Haskell 的$不是一种 apply 函数——它更像funcall,除了限于一个参数,这使得它在 Lisp 中无用。 (但在 Haskell 中,单参数限制显然不是问题。)
          • 顺便说一句,你不会编译,我测试过。看我的回答
          猜你喜欢
          • 2017-04-30
          • 1970-01-01
          • 1970-01-01
          • 2011-11-03
          • 1970-01-01
          • 2010-10-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多