Wnioskowanie typu na podstawie typów produktów

15

Pracuję nad kompilatorem dla języka konkatenatywnego i chciałbym dodać obsługę wnioskowania typu. Rozumiem Hindleya-Milnera, ale nauczyłem się teorii typów, więc nie jestem pewien, jak ją dostosować. Czy następujący system jest dźwiękowy i można go w sposób zdecydowanie wywnioskować?

Termin jest literałem, kompozycją terminów, cytatem terminu lub prymitywem.

e::=x|ee|[e]|

Wszystkie terminy oznaczają funkcje. Dla dwóch funkcji i e 2 , e 1e1e2e1e2=e2e1 , to znaczy zestawienie oznacza skład odwrotny. Literały oznaczają funkcje niladyczne.

Terminy inne niż skład mają podstawowe zasady dotyczące typów:

x:ι[Lit]Γe:σΓ[e]:α.ασ×α[Quot],α not free in Γ

Szczególnie nie ma zasad stosowania, ponieważ brakuje im języków konkatenatywnych.

Typ jest literałem, zmienną typu lub funkcją od stosów do stosów, gdzie stos jest zdefiniowany jako krotka zagnieżdżona z prawej strony. Wszystkie funkcje są domyślnie polimorficzne w odniesieniu do „reszty stosu”.

τ::=ι|α|ρρρ::=()|τ×ρσ::=τ|α.σ

To pierwsza rzecz, która wydaje się podejrzana, ale nie wiem dokładnie, co jest z nią nie tak.

Aby pomóc czytelności i obniżyć nawiasach, będę zakładać, że ab=b×(a) w schematach typów. Użyję również dużej litery do zmiennej oznaczającej stos, a nie do pojedynczej wartości.

Istnieje sześć prymitywów. Pierwsze pięć jest dość nieszkodliwe. dupprzyjmuje najwyższą wartość i tworzy dwie jej kopie. swapzmienia kolejność dwóch najwyższych wartości. popodrzuca najwyższą wartość. quoteprzyjmuje wartość i tworzy cytat (funkcję), który ją zwraca. applystosuje ofertę do stosu.

dup::Ab.AbAbbswap::Abc.AbcAcbpop::Ab.AbAquote::Ab.AbA(C.CCb)apply::AB.A(AB)B

Ostatni kombinator composepowinien wziąć dwa cytaty i zwrócić rodzaj ich konkatenacji, czyli . W statycznie typowanym języku konkatenatywnymCattypjest bardzo prosty.[e1][e2]compose=[e1e2]compose

compose::ABCD.A(BC)(CD)A(BD)

Jednak ten typ jest zbyt restrykcyjny: wymaga, aby produkcja pierwszej funkcji dokładnie odpowiadała zużyciu drugiej. W rzeczywistości musisz założyć różne typy, a następnie je zunifikować. Ale jak byś napisał ten typ?

compose::ABCDE.A(BC)(DE)A

Jeśli pozwolisz oznaczać różnicę dwóch typów, to myślę, że możesz napisać typ poprawnie.compose

compose::ABCDE.A(BC)(DE)A((DC)B((CD)E))

Jest wciąż stosunkowo prosta: composewykonuje funkcję i jeden f 2 : D E . Jego wynik zużywa B na szczycie zużycia f 2 niewyprodukowanego przez f 1 , i daje D na szczycie produkcji f 1 niewykorzystanego przez f 2 . Daje to regułę dla zwykłego składu.f1:BCf2:DEBf2f1Df1f2

Γe1:AB.ABΓe2:CD.CDΓe1e2:((CB)A((BC)D))[Comp]

Jednak nie wiem, czy ta hipotetyczna rzeczywistości coś odpowiada, i gonię ją w kółko na tyle długo, że myślę, że źle skręciłem. Czy może to być zwykła różnica krotek?

A.()A=()A.A()=AABCD.ABCD=BD iff A=Cotherwise=undefined

Is there something horribly broken about this that I’m not seeing, or am I on something like the right track? (I’ve probably quantified some of this stuff wrongly and would appreciate fixes in that area as well.)

Jon Purdy
źródło
How do you use variables in your grammar? This question should help you in handling the "subtyping" you seem to need.
jmad
1
@jmad: I’m not sure I understand the question. Type variables are just there for the sake of formally defining type schemes, and the language itself doesn’t have variables at all, just definitions, which can be [mutually] recursive.
Jon Purdy
Fair enough. Can you say why (perhaps with an example) the rule for compose is too restrictive? I have the impression that this is fine like this. (e.g. the restriction C=D could be handled by unification like for application in like in the λ-calculus)
jmad
@jmad: Sure. Consider twice defined as dup compose apply, which takes a quotation and applies it twice. [1 +] twice is fine: you’re composing two functions of type ιι. But [pop] twice is not: if Ab.f1,f2:AbA, the problem is that AAb, so the expression is disallowed even though it ought to be valid and have type Ab.AbbA. The solution is of course to put the qualifier in the right place, but I’m mainly wondering how to actually write the type of compose without some circular definition.
Jon Purdy

Odpowiedzi:

9

The following rank-2 type

compose:ABCδ.δ (α.α AαB) (β.β BβC)δ (γ.γ AγC)
seems to be sufficiently general. It is much more polymorphic than the type proposed in the question. Here variable quantify over contiguous chunks of stack, which captures multi-argument functions.

Greek letters are used for the rest-of-the-stack variables for clarity only.

It expresses the constraints that the output stack of the first element on the stack needs to be the same as the input stack of the second element. Appropriately instantiating the variable B for the two actually arguments is the way of getting the constraints to work properly, rather than defining a new operation, as you propose in the question.

Type checking rank-2 types is undecidable in general, I believe, though some work has been done that gives good results in practice (for Haskell):

  • Simon L. Peyton Jones, Dimitrios Vytiniotis, Stephanie Weirich, Mark Shields: Practical type inference for arbitrary-rank types. J. Funct. Program. 17(1): 1-82 (2007)

The type rule for composition is simply:

Γe1:α.α Aα BΓe1:α.α Bα CΓe1 e2:α.α Aα C

To get the type system to work in general, you need the following specialisation rule:

Γe:α.α Aα BΓe:α.C Aα C B
Dave Clarke
źródło
Thanks, this was very helpful. This type is correct for functions of a single argument, but it doesn’t support multiple arguments. For instance, dup + should have type ιι because + has type ιιι. But type inference in the absence of annotations is an absolute requirement, so clearly I need to go back to the drawing board. I have an idea for another approach to pursue, though, and will blog about it if it works out.
Jon Purdy
1
The stack types quantify over stack fragments, so there is no problem dealing with two argument functions. I'm not sure how this applies to dup +, as that does not use compose, as you defined it above.
Dave Clarke
Er, right, I meant [dup] [+] compose. But I read αB as B×α; say B=ι×ι; then you have (ι×ι)×α and not ι×(ι×α). The nesting isn’t right, unless you flip the stack around so that the top is the last (deepest nested) element.
Jon Purdy
I may be building my stack in the wrong direction. I don't think the nesting matters, so long as the pairs building up the stack do not appear in the programming language. (I'm planning to update my answer, but need to do a little research first.)
Dave Clarke
Yeah, nesting is pretty much an implementation detail.
Jon Purdy