Search code examples
ocaml

A list of types from modules sharing the same signature?


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?


Solution

  • 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 = "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"
    

    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 ""
              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"
    

    As an added note, it is not strictly speaking necessary for these classes to inherit from 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"
    

    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 -> 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
    

    Note that OCaml had no problem with the definition of d without the encode method until I tried to use it. Then it rightly 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 -> 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
    

    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