Como decidir se deve ser parametrizado no nível de tipo ou no nível do módulo ao projetar módulos?

Estou trabalhando para uma compreensão profunda dos módulos no estilo ML: acho que o conceito é importante e adoro o tipo de pensamento que eles incentivam. Agora, estou descobrindo a tensão que pode surgir entre tipos paramétricos e módulos paramétricos. Estou procurando ferramentas para pensar sobre o assunto que me ajudará a tomar decisões de design inteligente à medida que desenvolvo meus programas.

Primeiro vou tentar descrever minha pergunta em geral. Depois, darei um exemplo concreto de um projeto de aprendizado em que estou trabalhando. Finalmente, revisitarei a questão geral, a fim de levá-la a um ponto.

(Sinto muito por ainda não saber o suficiente para fazer essa pergunta de maneira mais sucinta.)

Em termos gerais, a tensão que descobri é a seguinte: as funções são mais flexíveis e abertas à reutilização mais ampla, quando fornecemos assinaturas de tipo paramétrico (quando apropriado). No entanto, os módulos são mais flexíveis e abertos à reutilização mais ampla quando selamos a parametrização de funções dentro do módulo e parametrizamos o módulo inteiro em um determinado tipo.

Um exemplo pronto dessa diferença pode ser encontrado na comparação de módulos que implementam oLIST assinatura com aqueles que implementamORD_SET. Um móduloList:LIST fornece várias funções úteis, parametrizadas em qualquer tipo. Depois de definir ou carregar umList No módulo, podemos aplicar prontamente qualquer uma das funções que ele fornece para construir, manipular ou examinar listas de qualquer tipo. Por exemplo, se estivermos trabalhando com seqüências de caracteres e números inteiros, podemos usar um e o mesmo módulo para construir e manipular valores dos dois tipos:

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

Por outro lado, se queremos lidar com conjuntos ordenados, as questões são diferentes: conjuntos ordenados exigem que uma relação de ordenação seja mantida sobre todos os seus elementos e não pode haver uma função concreta únicacompare : 'a * 'a -> order produzindo essa relação para todo tipo. Conseqüentemente, exigimos um módulo diferente que atenda àsORD_SET assinatura para cada tipo que queremos colocar em conjuntos ordenados. Assim, para construir ou manipular conjuntos ordenados de seqüências de caracteres e números inteiros, devemos implementar módulos diferentes para cada tipo [1]:

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

E devemos então usar a função de ajuste do módulo apropriado quando desejarmos operar em um determinado tipo:

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

Há uma troca bastante direta aqui:LIST Os módulos fornecem funções que variam sobre qualquer tipo que você queira, mas não podem tirar proveito de nenhuma relação que ocorra entre os valores de qualquer tipo específico;ORD_SET Os módulos fornecem funções que são necessariamente restritas ao tipo fornecido no parâmetro functors, mas através dessa mesma parametrização, eles são capazes de incorporar informações específicas sobre a estrutura interna e as relações de seus tipos de destino.

É fácil imaginar casos em que desejaríamos projetar uma família alternativa de módulos de lista, usando functors para parametrizar tipos e outros valores para fornecer tipos de dados semelhantes a listas com uma estrutura mais complicada: por exemplo, para especificar um tipo de dados para a lista ordenada , ou para representar listas usando árvores de pesquisa binária com auto balanceamento.

Ao criar um módulo, também acho fácil reconhecer quando ele poderá fornecer funções polimórficas e quando precisará ser parametrizado em alguns tipos. O que parece mais difícil, para mim, é descobrir em quais tipos de módulos você deve confiar ao trabalhar em algo mais adiante.

Em termos gerais, minha pergunta é a seguinte: quando estou projetando um sistema de módulos relacionados de várias formas, como posso determinar se devo projetar módulos que fornecem funções polimórficas ou módulos gerados usando functores parametrizados em tipos e valores?

Espero ilustrar o dilema e por que isso importa com o exemplo a seguir, retirado de um projeto de brinquedo no qual estou trabalhando.

eu tenho umfunctor PostFix (ST:STACK) : CALCULATOR_SYNTAX. Isso leva a uma implementação de uma estrutura de dados da pilha e produz um analisador que lê a notação postfix concreta ("polonês reverso") em uma sintaxe abstrata (a ser avaliada por um módulo de calculadora no fluxo abaixo) e vice-versa. Agora, eu estava usando uma interface de pilha padrão que fornece um tipo de pilha polimórfica e número de funções para operar nela:

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

Isso funciona bem e me dá alguma flexibilidade, pois eu posso usar uma pilha baseada em lista ou pilha baseada em vetor ou qualquer outra coisa. Mas, digamos que eu queira adicionar uma função de registro simples ao módulo da pilha, para que toda vez que um elemento seja enviado ou retirado da pilha, ele imprima o estado atual da pilha. Agora vou precisar de umfun toString : 'a -> string para o tipo coletado pela pilha, e isso, pelo que entendi, não pode ser incorporado aoSTACK módulo. Agora preciso selar o tipo no módulo e parametrizar o módulo sobre o tipo coletado na pilha e umtoString função que me permitirá produzir uma representação imprimível do tipo coletado. Então eu preciso de algo como

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

e isso vainão produzir um módulo correspondente aoSTACK assinatura, já que não fornece um tipo polimórfico. Portanto, devo alterar a assinatura necessária para oPostFix functor. Se eu tenho muitos outros módulos, tenho que mudar todos eles também. Isso pode ser inconveniente, mas o verdadeiro problema é que eu não posso mais usar meu simples baseado em lista ou em vetor.STACK módulos noPostFix functor quando eunão deseja log. Agora, ao que parece, eu tenho que voltar e reescrever esses módulos para ter um tipo selado também.

Então, para retornar, expandir e (misericordiosamente) terminar minha pergunta:

Existe alguma maneira de especificar a assinatura dos módulos produzidos porStackFn para que eles acabem como "casos especiais" deSTACK?Como alternativa, existe uma maneira de escrever uma assinatura para oPostFix módulo que permitiria os dois módulos produzidos porStackFn e aqueles que satisfazemSTACK?De um modo geral, existe uma maneira de pensar sobre a relação entre os módulos que me ajudaria a entender / antecipar esse tipo de coisa no futuro?

(Se você leu até aqui. Muito obrigado!)

questionAnswers(1)

yourAnswerToTheQuestion