Wie entscheiden Sie, ob Sie beim Entwerfen von Modulen auf Typ- oder Modulebene parametrieren?

Ich arbeite auf ein tiefes Verständnis für ML-Module hin: Ich denke, das Konzept ist wichtig und ich liebe die Art des Denkens, die sie fördern. Ich entdecke gerade die Spannung, die zwischen parametrischen Typen und parametrischen Modulen entstehen kann. Ich bin auf der Suche nach Tools, mit denen ich bei der Entwicklung meiner Programme Entscheidungen über intelligentes Design treffen kann.

Fist Ich werde versuchen, meine Frage allgemein zu beschreiben. Dann werde ich ein konkretes Beispiel aus einem Lernprojekt geben, an dem ich arbeite. Abschließend werde ich noch einmal auf die allgemeine Frage eingehen, um sie auf einen Punkt zu bringen.

(Es tut mir leid, dass ich noch nicht genug weiß, um diese Frage prägnanter zu stellen.)

m Allgemeinen habe ich folgende Spannung festgestellt: Funktionen sind am flexibelsten und können am häufigsten wiederverwendet werden, wenn sie mit parametrischen Typensignaturen versehen werden (sofern zutreffend). Module sind jedoch am flexibelsten und können am häufigsten wiederverwendet werden, wenn wir die Parametrisierung von Funktionen innerhalb des Moduls versiegeln und stattdessen das gesamte Modul für einen bestimmten Typ parametrisieren.

in gutes Beispiel für diesen Unterschied ist der Vergleich von Modulen, die das @ implementiereLIST Signatur mit denen, die @ implementierORD_SET. Ein ModulList:LIST bietet eine Reihe nützlicher Funktionen, die für jeden Typ parametriert sind. Sobald wir ein @ definiert oder geladen habListit dem @ -Modul können wir alle Funktionen, die es bietet, leicht anwenden, um Listen jeglichen Typs zu erstellen, zu manipulieren oder zu untersuchen. Wenn wir beispielsweise sowohl mit Zeichenfolgen als auch mit ganzen Zahlen arbeiten, können wir ein und dasselbe Modul verwenden, um Werte beider Typen zu konstruieren und zu bearbeiten:

val strList = List.@ (["a","b"], ["c","d"])
val intList = List.@ ([1,2,3,4], [5,6,7,8])

enn wir uns andererseits mit geordneten Mengen befassen wollen, sind die Dinge anders: Bei geordneten Mengen muss eine Ordnungsbeziehung über alle ihre Elemente hinweg bestehen, und es kann keine einzelne konkrete Funktion gebecompare : 'a * 'a -> order Diese Beziehung für jeden Typ herstellen. Aus diesem Grund benötigen wir ein anderes Modul, das das @ -Zeichen erfüllORD_SET Signatur für jeden Typ, den wir in geordnete Sets einteilen möchten. Um also geordnete Mengen von Zeichenfolgen und ganzen Zahlen zu konstruieren oder zu manipulieren, müssen wir für jeden Typ verschiedene Module implementieren [1]:

structure IntOrdSet = BinarySetFn ( type ord_key = int
                                    val compare = Int.compare )
structure StrOrdSet = BinarySetFn ( type ord_key = string
                                    val compare = String.compare )

Und wir müssen dann die Anpassungsfunktion des entsprechenden Moduls verwenden, wenn wir mit einem bestimmten Typ arbeiten möchten:

val strSet = StrOrdSet.fromList ["a","b","c"]
val intSet = IntOrdSet.fromList [1,2,3,4,5,6]

Es gibt einen ziemlich einfachen Kompromiss hier:LIST -Module bieten Funktionen, die sich über einen beliebigen Typ erstrecken. Sie können jedoch keine Beziehungen nutzen, die zwischen den Werten eines bestimmten Typs bestehen.ORD_SET -Module stellen Funktionen bereit, die notwendigerweise auf den im Parameter functors angegebenen Typ beschränkt sind. Durch dieselbe Parametrisierung können sie jedoch spezifische Informationen über die interne Struktur und die Beziehungen ihrer Zieltypen einbeziehen.

Es ist leicht vorstellbar, dass wir eine alternative Familie von Listenmodulen entwerfen möchten, indem wir mithilfe von Funktoren Typen und andere Werte parametrisieren, um listenähnliche Datentypen mit einer komplizierteren Struktur zu versehen: z. B. um einen Datentyp für die Bestellung anzugeben list oder um Listen mit selbstausgleichenden binären Suchbäumen darzustellen.

Bei der Erstellung eines Moduls ist es meines Erachtens auch recht einfach zu erkennen, wann es polymorphe Funktionen bereitstellen kann und wann es auf einem oder mehreren Typen parametrisiert werden muss. Was mir schwieriger erscheint, ist herauszufinden, auf welche Art von Modulen Sie sich verlassen sollten, wenn Sie weiter unten an etwas arbeiten.

enerell lautet meine Frage wie folgt: Wenn ich ein System aus verschiedenen verwandten Modulen entwerfe, wie kann ich dann herausfinden, ob Module mit polymorphen Funktionen oder Module, die mithilfe von auf Typen und Werten parametrisierten Funktoren generiert wurden, entworfen werden solle

Ich hoffe, das Dilemma und die Gründe dafür anhand des folgenden Beispiels veranschaulichen zu können, das aus einem Spielzeugprojekt stammt, an dem ich gerade arbeite.

Ich habe einfunctor PostFix (ST:STACK) : CALCULATOR_SYNTAX. Dies erfordert eine Implementierung einer Stack-Datenstruktur und erzeugt einen Parser, der die konkrete Postfix-Notation ("reverse polish") in einer abstrakten Syntax liest (die von einem Rechnermodul nachgelagert ausgewertet werden muss) und umgekehrt. Jetzt hatte ich eine Standard-Stapelschnittstelle verwendet, die einen polymorphen Stapeltyp und eine Reihe von Funktionen zur Verfügung stellt, um damit zu arbeiten:

signature STACK =
sig
    type 'a stack
    exception EmptyStack

    val empty : 'a stack
    val isEmpty : 'a stack -> bool

    val push : ('a * 'a stack) -> 'a stack
    val pop  : 'a stack -> 'a stack
    val top  : 'a stack -> 'a
    val popTop : 'a stack -> 'a stack * 'a
end

Dies funktioniert gut und gibt mir Flexibilität, da ich einen listenbasierten Stapel oder einen vektorbasierten Stapel oder was auch immer verwenden kann. Angenommen, ich möchte dem Stapelmodul eine einfache Protokollierungsfunktion hinzufügen, damit jedes Mal, wenn ein Element in den Stapel verschoben oder aus ihm entfernt wird, der aktuelle Status des Stapels ausgedruckt wird. Jetzt brauche ich einfun toString : 'a -> string für den vom Stapel gesammelten Typ, und dies kann, wie ich verstehe, nicht in das @ aufgenommen werdSTACK Modul. Jetzt muss ich den Typ in das Modul einschließen und das Modul über den im Stapel gesammelten Typ und ein @ parametrisieretoString -Funktion, mit der ich eine druckbare Darstellung des gesammelten Typs erstellen kann. Also brauche ich so etwas wie

functor StackFn (type t
                 val toString: t -> string ) =
struct
   ...
end

und das wirdnich ein Modul erzeugen, das dem @ entspricSTACK Signatur, da sie keinen polymorphen Typ liefert. Daher muss ich die für das @ erforderliche Signatur ändePostFix functor. Wenn ich viele andere Module habe, muss ich auch alle ändern. Das könnte unbequem sein, aber das eigentliche Problem ist, dass ich mein einfaches listenbasiertes oder vektorbasiertes @ nicht mehr verwenden kanSTACK Module in derPostFix functor wenn ich nicht wollen protokollieren. Nun muss ich anscheinend zurückgehen und diese Module neu schreiben, um auch einen versiegelten Typ zu haben.

So kehren Sie zu meiner Frage zurück, erweitern Sie sie und beenden Sie sie (gnädigerweise):

Gibt es eine Möglichkeit, die Signatur der Module anzugeben, die von @ erstellt wurdeStackFn so dass sie als "Sonderfälle" von @ endSTACK?Alternativ gibt es eine Möglichkeit, eine Signatur für das @ zu schreibPostFix -Modul, das beide von @ produzierten Module zuläsStackFn und diejenigen, die @ erfüllSTACK?Generell gesagt, gibt es eine Möglichkeit, über die Beziehung zwischen Modulen nachzudenken, die mir helfen würde, solche Dinge in Zukunft zu fangen / zu antizipieren?

(Wenn Sie so weit gelesen haben. Vielen Dank!)

Antworten auf die Frage(2)

Ihre Antwort auf die Frage