具有相同签名的模块中的类型列表?

huangapple go评论50阅读模式
英文:

A list of types from modules sharing the same signature?

问题

I can help translate the code-related content for you:

我试图创建一堆类型来生成一个MIDI文件以在单独的音频播放器中播放是的我知道有可用的库...我正在用这种方法学习)。我创建了一个单一的模块签名在尝试函数器解决方案时),包含这些类型的各种模块正在实现签名看起来像这样暂时是这样的...一旦我得到一个工作的模板字符串将被更改为字节):

module type MIDIInterface =
    sig
        type t
        val encode : t -> string
    end

我在不同的模块/文件中创建的各种类型将用于各种MidiEventsNoteOnNoteOff等它们都会有一个encode函数因为每种类型都不同不同数量的变量不同的事件代码等),它们不能只使用相同的encode函数

我最终想要做的是有一种方法可以遍历歌曲的所有事件并调用每个事件的encode函数来创建完整的字符串最终是字节字符串),然后可以保存到文件中

我知道我可以只创建一个GADT以将所有不同的类型都变成相同的类型以便可以将它们转换成一个巨大的列表

type Event =
   | NoteOn  of NoteOnEvent
   | NoteOff of NoteOffEvent
    ...

...但这似乎有点臃肿主要是因为我每次创建新类型时都必须更新该类型我也知道我可以只在一个类型下实现它们然后只有一个巨大的encode函数

let encode = function
    | (NoteOn time note velocity) -> ...
    | (NoteOff time note) -> ...

这是我在Haskell中首次尝试的解决方案当我查找引用时阅读起来非常困难这也是我想将它们分成不同模块中的不同类型的一个重要原因)。 

在OCaml中是否有更好的解决方案可以根据相同的签名统一这些不同类型而不必维护一个大型组合类型将其转换为某种列表/数组/集合以便我可以"fold encode empty collection_of_events"如果有的话您能告诉我这叫什么以及我可以在哪里了解它是如何工作的吗
英文:

I'm trying to create a bunch of types to generate a MIDI file to be played in a separate audio player (yes, I know there are libraries available... I'm using this as a way to learn). I created a single module signature (while attempting a functor solution) that the various modules containing these types are implementing. The signature looks like this (for now... string will be changed to bytes once I get a working template):

module type MIDIInterface =
    sig
        type t
        val encode : t -> string
    end

The various types I'm creating in different modules/files would be for the various types of MidiEvents: NoteOn, NoteOff, etc. All of them will have an encode function. Because each type is different (differing # of variables, differing event code, etc.), they can't just use the same encode function.

What I want to ultimately do is have a way to fold through all of the events of a song and call each events' encode function to create the full string (eventually bytestring) that can then be saved to a file.

I know I can just create a GADT to put make all of the various types the same type so that I can turn them into a giant list:

type Event =
   | NoteOn  of NoteOnEvent
   | NoteOff of NoteOffEvent
    ...

...but this seems kind of a bulky solution, primarily because I'd have to update that type every time I create a new type. I also know that I can just implement them all under one type and just have one giant encode function:

let encode = function
    | (NoteOn time note velocity) -> ...
    | (NoteOff time note) -> ...

This is the solution I first tried in Haskell, and it was a nightmare to try to read through it when I was looking to reference something (one of the big reasons I'd like to break them into different types in different modules).

Is there a better solution in OCaml to be able to unify these various types based off the same signature without maintaining a big combinatory type into some sort of list/array/collection so that I can "fold encode empty collection_of_events"? If so, can you please let me know what this is called and where I can read up on how it works?

答案1

得分: 3

这部分是您提供的代码翻译:

看起来你可能正在寻找 OCaml 的[面向对象](https://ocaml.org/docs/objects) 方面。虚拟类 `midi_interface` 本身不能被实例化,因此类似于你的模块类型。由于你的模块类型 `MIDIInterface` 将创建抽象类型,`a` 和 `b` 子类的实现细节是不透明的。

```ocaml
# class virtual midi_interface =
    object
      method virtual encode : string
    end

  class a =
    object
      inherit midi_interface
      method encode = "a"
    end

  class b =
    object
      inherit midi_interface
      method encode = "b"
    end

  let lst : midi_interface list = [new a; new b]
  let result = List.fold_left (fun i x -> i ^ x#encode) "" lst
;;
class virtual midi_interface :
  object method virtual encode : string end
class a : object method encode : string end
class b : object method encode : string end
val lst : midi_interface list = [<obj>; <obj>]
val result : string = "ab"

并且展示了具有不同变量的能力:

# class c n =
    object
      inherit midi_interface
      method encode =
        let rec replicate s n =
          if n = 0 then ""
          else s ^ replicate s (n - 1)
        in
        replicate "c" n
    end;;
class c : int -> object method encode : string end
# let lst : midi_interface list = [new a; new b; new c 5] in
  List.fold_left (fun i x -> i ^ x#encode) "" lst;;
- : string = "abccccc"

另外,请注意,这些类不一定需要继承自 midi_interface

# class d = object
    method encode = "d"
  end;;
class d : object method encode : string end
# let lst : midi_interface list = [new a; new b; new c 5; new d] in
  List.fold_left (fun i x -> i ^ x#encode) "" lst;;
- : string = "abcccccd"

现在,如果我没有为 d 定义 encode 会发生什么?

# class d = object
  end;;
class d : object  end
# let lst : midi_interface list = [new a; new b; new c 5; new d] in
  List.fold_left (fun i x -> i ^ x#encode) "" lst;;
Error: This expression has type d
       but an expression was expected of type midi_interface
       The first object type has no method encode

请注意,OCaml 在没有 encode 方法的情况下定义 d 是没有问题的,直到我尝试使用它。然后它就抗议了。即使我删除了类型注释,它也会抗议缺少 encode

# let lst = [new a; new b; new c 5; new d] in
  List.fold_left (fun i x -> i ^ x#encode) "" lst;;
Error: This expression has type d
       but an expression was expected of type a
       The first object type has no method encode

但是,如果我明确表示 d 继承自 midi_interface,那么编译器会在我尝试 定义 d 而不是在我尝试 使用 它时抱怨,如果我没有实现 encode

# class d = object
    inherit midi_interface
  end;;
Error: This non-virtual class has virtual methods.
       The following methods are virtual : encode
英文:

It sounds like you may be looking for the object-oriented side of OCaml. The virtual class midi_interface cannot itself be instantiated and thus is analogous to your module type. As your module type MIDIInterface will create abstract types, the implementation details of the a and b subclasses are opaque.

# class virtual midi_interface =
    object
      method virtual encode : string
    end

  class a =
    object
      inherit midi_interface
      method encode = &quot;a&quot;
    end

  class b =
    object
      inherit midi_interface
      method encode = &quot;b&quot;
    end

  let lst : midi_interface list = [new a; new b]
  let result = List.fold_left (fun i x -&gt; i ^ x#encode) &quot;&quot; lst
;;
class virtual midi_interface :
  object method virtual encode : string end
class a : object method encode : string end
class b : object method encode : string end
val lst : midi_interface list = [&lt;obj&gt;; &lt;obj&gt;]
val result : string = &quot;ab&quot;

And demonstrating the ability to have different variables:

# class c n =
    object
      inherit midi_interface
      method encode =
        let rec replicate s n =
          if n = 0 then &quot;&quot;
          else s ^ replicate s (n - 1)
        in
        replicate &quot;c&quot; n
    end;;
class c : int -&gt; object method encode : string end
# let lst : midi_interface list = [new a; new b; new c 5] in
  List.fold_left (fun i x -&gt; i ^ x#encode) &quot;&quot; lst;;
- : string = &quot;abccccc&quot;

As an added note, it is not strictly speaking necessary for these classes to inherit from midi_interface.

# class d = object
    method encode = &quot;d&quot;
  end;;
class d : object method encode : string end
# let lst : midi_interface list = [new a; new b; new c 5; new d] in
  List.fold_left (fun i x -&gt; i ^ x#encode) &quot;&quot; lst;;
- : string = &quot;abcccccd&quot;

Now, what happens if I fail to define encode for d?

# class d = object
  end;;
class d : object  end
# let lst : midi_interface list = [new a; new b; new c 5; new d] in
  List.fold_left (fun i x -&gt; i ^ x#encode) &quot;&quot; lst;;
Error: This expression has type d
       but an expression was expected of type midi_interface
       The first object type has no method encode

Note that OCaml had no problem with the definition of d without the encode method until I tried to use it. Then it right protested. It would protest the lack of encode even if I removed the type annotation.

# let lst = [new a; new b; new c 5; new d] in
  List.fold_left (fun i x -&gt; i ^ x#encode) &quot;&quot; lst;;
Error: This expression has type d
       but an expression was expected of type a
       The first object type has no method encode

But, if I explicitly say that d inherits from midi_interface the compiler will complain if I fail to implement encode as soon as I try to define d rather than when I try to use it.

# class d = object
    inherit midi_interface
  end;;
Error: This non-virtual class has virtual methods.
       The following methods are virtual : encode

答案2

得分: 2

@Chris提供了一个非常好的、可工作的答案,但我觉得还可以补充一些内容。在我提供更多细节之前,先简单了解一下软件设计的相关知识。

大致上,您可以在软件扩展方面有两个“方向”,即添加不同类型的数据或添加不同操作数据的方式。这分别称为纵向扩展和横向扩展(我不会深入讨论这些名称的细节)。

OCaml通常更青睐横向扩展:一旦定义了一个涵盖程序必须处理的所有可能数据的总和类型,就可以很容易地添加对该数据进行操作的新函数(使用match)。最重要的是,这是一种局部变更,不需要修改代码的其余部分。事实上,其余的代码可能已经被预编译,这并不妨碍您添加函数。另一方面,如果您想添加新类型的数据,您将不得不修改原始类型(以添加新情况)以及操作它们的每个函数,因为它们的模式匹配将不再是详尽无遗的。这是一种非局部变更,您需要项目的其余源代码来进行小的补充。

相比之下,面向对象编程语言通常更好地支持纵向扩展。通过继承现有类,很容易扩展现有类型层次结构。同样,这是一种局部变更(只是您添加的代码),甚至可以与预编译的内容一起使用。然而,添加方法现在是昂贵的,因为您必须修改已经定义的每个类(这是非局部的),对于大多数编译语言来说,这不能在预编译的代码块上完成。

例如,考虑一个包含一些实体(玩家、绵羊等)的游戏。在OCaml中,您可以将所有实体表示为一个大枚举(type entity = Player of ... | Sheep of ... | ...),而在面向对象的语言中,您可能更倾向于使用类层次结构。横向扩展可能是,每个实体都可以发出一些声音的可能性。在OCaml中,您可以定义一个function Player _ -&gt; &quot;claps&quot; | Sheep _ -&gt; &quot;bleat&quot; | ...,而在面向对象的语言中,您需要为每个现有类添加一个方法。纵向扩展可以是添加一个新实体,比如僵尸。在OCaml中,这需要更改entity类型对其进行模式匹配的每个函数,而在面向对象的语言中,这只意味着创建一个新类。

事实上,OCaml支持(正如我们将看到的)设计易于垂直扩展的软件的多种方式,而不是水平扩展。这个初步说明的主要要点是:当您选择用哪种技术解决问题时,您应该考虑的第一件事是,您想要轻松进行哪种类型的扩展。我将列举的方法在其他方面也存在折衷,但在我将列举的方法中,我认为这些折衷相对较小,与您易于执行的修改类型有关。

另一个需要注意的事情是,无论您选择的方法是什么,您最终都将写大致相同数量的代码,只是组织方式不同。

英文:

@Chris has provided a very good, working answer, but I feel something can be added. Quick detour about software design before I give more details.

Roughly, there are two "directions" in which you could expand your software, which is either adding different kinds of data, or adding different ways of operating of data. These are called, respectively, vertical and horizontal extensions (I won't get into the details about these names).

OCaml usually favors horizontal extensions: once a sum type has been defined, which covers all the possible data that your program has to process, it's easy to add new functions that operate on that data (it's a match). Most importantly, it's a local change, that doesn't require modifying the rest of the code. In fact, the rest of the code could very well be precompiled, that doesn't prevent you from adding a function. On the other hand, if you wanted to add new kind of datatypes, you'd have to modify the original type (to add a new case) and every function that operates on them, because their pattern matching will no longer be exhaustive. This is a non-local change, and you need the source code of the rest of the project to make a small addendum.

OO programing languages, on the contrary, usually support vertical extensions better. It's easy to extend an existing type hierarchy by inheriting an existing class. Again, it's a local change (just the code you add), and it could even work with precompiled stuff. However, adding a method is now costly, because you'd have to modify every class already defined (which is non-local) and, for most compiled languages, that can't be done on precompiled blobs.

As an example, think of a game with some entities (a player, sheeps, ...). In OCaml, you could represent all the entities in a big enum (type entity = Player of ... | Sheep of ... | ...) whereas in an OO language, you'd rather use a class hierarchy. A horizontal extension would be, for instance, the possibility for each entity to make some sounds. In OCaml, you'd define a function Player _ -&gt; &quot;claps&quot; | Sheep _ -&gt; &quot;bleat&quot; | ..., whereas in an OO language, you'd have to add a method to each existing class. A vertical extension could be adding a new entity, say, a zombie. In OCaml, that would require changing the type entity and every function that pattern matches on it, whereas in an OO language, that would just mean creating a new class.

In fact, OCaml supports (as we will see) several ways to design software that is easily extensible vertically, instead of horizontally. The main point of this preliminary explanation was: when you choose which technique you use to solve your problem, the first thing that you should consider is what kind of extensions you want to make easy. There are other tradeoffs in the approaches I will enumerate, but IMO they are minor with respect what kind of modifications you make easy to perform.

An other thing to note is that, whatever the approach you will chose, you will eventually write more or less the same amount of code, it will just be grouped differently.


You, evidently, already know about algebraic datatypes, which are the most straightforward way to encode data. As you have noted, however, using this way favors horizontal extensions, which means grouping data types on the one side, and functions on the other side. This is very efficient (with respect to what we will see later on).

I'll just drop a line here about polymorphic variants, which allow a little more freedom, but come at a (usually negligible) additional cost. They can serve as a middle man between ADT and what comes next, but since it's likely not the best solution specifically for the question you asked, I won't add more details.


As I mentioned earlier, there is more than one solution to make vertical extensions easier (the obvious one being OOP). I will talk about two of them; both of them rely on dynamic dispatch, that is, type erasure (we forget about the exact type during the compilation) coupled with some runtime information to compensate for the information loss at compile time.

The first way of doing this is with OOP. For your information, OOP in OCaml is very different from what it looks like in other languages (classes are not types, objects are not bound to a specific class but, instead, are simply typed according to what they can do, ...). For this reason, it integrates pretty well with the style of the rest of the language. OCaml, in this aspect as in many others, is pragmatic instead of trying to be "pure" or elegant.

In OOP, the type erasure works with subtyping. If A is a subtype of B, then any value a: A can be though of as a value of type B (typically, any specific cat can be though as a cat, or as an animal, which is less specific). Since the true, underlying type of a given object is never truly known (because anyone could have created a subclass of the class you where expecting), it's impossible to resolve at compile time which methods should be called. Instead, each object carries some metadata which allows computing at runtime the actual function to be called on a method dispatch.

However, in OCaml, there is an other way to have dynamic dispatch: it's reified modules (or first-class modules, like in first-class functions). It's a terrific feature, that allows you to turn any module into a regular value. Before going into details, I'd like to show you the same example as @Chris, but done with modules:

#
module type MidiInterface = sig
  val encode : unit -&gt; string
end

module A: MidiInterface = struct
  let encode () = &quot;a&quot;
end

module B: MidiInterface = struct
  let encode () = &quot;b&quot;
end

let lst : (module MidiInterface) list = [(module A); (module B)]
let result = List.fold_left (fun i (module X: MidiInterface) -&gt; i ^ X.encode ()) &quot;&quot; lst;;
module type MidiInterface = sig val encode : unit -&gt; string end
module A : MidiInterface
module B : MidiInterface
val lst : (module MidiInterface) list = [&lt;module&gt;; &lt;module&gt;]
val result : string = &quot;ab&quot;
#

and

#
let c : int -&gt; (module MidiInterface) = fun n -&gt; (module struct
  let encode () =
    let rec replicate s n =
      if n = 0 then &quot;&quot;
      else s ^ replicate s (n-1) in
    replicate &quot;c&quot; n
end);;
val c : int -&gt; (module MidiInterface) = &lt;fun&gt;
# let lst : (module MidiInterface) list = [(module A); (module B); c 5] in
List.fold_left (fun i (module X: MidiInterface) -&gt; i ^ X.encode ()) &quot;&quot; lst;;
- : string = &quot;abccccc&quot;
#

As you can see, it's quite natural (if you are acquainted with modules in OCaml) to "turn dynamic dispatch on" in otherwise "regular" OCaml code. Also note that, beside the few syntactic differences, my code and the one @Chris wrote are very similar. We both start with an abstract declaration of the information we want to erase at compiled time (here, the actual encode function / method). Then, we instantiate some values that satisfy the declaration, and erase they actual type. Finally, we dispatch the method / function call.

In general, modules are much, much more powerful than objects in OCaml, but that comes at a cost: note that most of the typing information I had to provide, contrary to what you are used to in OCaml, was not optional. Type inference of modules is much harder than usual type inference, and so we have to help the compiler a little more (which is, all in all, not that bad, since it's helpful for the reader). Furthermore, whereas with objects the syntax is very lightweight, with modules you have to explicitly tell OCaml when you want to "box" them as regular, opaque values, and when you want to retrieve the boxed module.


As a personal note, I have never seen objects being used in "real" OCaml code. I know that somewhere, someone uses that feature but, whenever I read (or wrote) code that was appropriate for OOP, modules where used instead, despite being more syntactically heavy. I'm not sure why. Maybe it's because modules work better with functors defined in other libraries.

huangapple
  • 本文由 发表于 2023年6月6日 10:17:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/76411040.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定