Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Co/contravariance for function schemas? #320

Open
nilern opened this issue Dec 14, 2020 · 1 comment
Open

Co/contravariance for function schemas? #320

nilern opened this issue Dec 14, 2020 · 1 comment

Comments

@nilern
Copy link
Contributor

nilern commented Dec 14, 2020

From working on coercion subtyping I would assume that a function schema encoder would be something like

(fn [f] (fn [x] (output-encoder (f (input-decoder x)))))

and decoder

(fn [f] (fn [x] (output-decoder (f (input-encoder x)))))

but currently they are no-ops.

Of course type-theoretic intuitions might not translate to a dynamic language. I think function schemas are a bit weird anyway since they can only be checked by calling the function.

@frenchy64
Copy link
Collaborator

This seems right. For example, encoding a string->int function to be int->string (psuedocode):

(encode [:=> [:cat string?] int?]
        str->int
        (function-transformer
          (cat-transformer (int-transformer))
          (string-transformer)))
=> (fn [& args]
     (encode int?
             (apply
               str->int
               (decode [:cat string?]
                       (cat-transformer (int-transformer))
                       args))
             (string-transformer)))

And decoding it back to string->int.

(decode [:=> [:cat string?] int?]
        int->str
        (function-transformer
          (cat-transformer (int-transformer))
          (string-transformer)))
=> (fn [& args]
     (decode int?
             (apply
               int->str
               (encode [:cat string?]
                       (cat-transformer (int-transformer))
                       args))
             (int-transformer)))

This might open a can of worms wrt. performance (eg., wrapping wrappers). There are some nice papers to mitigate some of the cost, see space-efficient contracts and coercions. An example written in Clojure.

Of course type-theoretic intuitions might not translate to a dynamic language. I think function schemas are a bit weird anyway since they can only be checked by calling the function.

Yes, perhaps. I think something weird (unsound) needs to be done for m/validate where no wrapping is allowed, but here we're allowed to use the standard contra/covariant rules used in higher-order contracts via wrapping. It seems sensible enough (not to comment on whether it should be included in malli :) ).

One interesting (new) part to me is the relationship between the schemas and the transformers. I tried to write some typing rules. As you say, this seems related to coercions, which I only have a passing familiarity with.

                    X->Y :- [:=> X Y]
(:decoder A-transformer) :- [:=> A X]
(:encoder B-transformer) :- [:=> Y B]
=====================================
(encode [:=> X Y]
        X->Y
        (function-transformer
          A-transformer
          B-transformer))
:- [:=> A B]
                    A->B :- [:=> A B]
(:encoder A-transformer) :- [:=> X A]
(:decoder B-transformer) :- [:=> B Y]
=====================================
(decode [:=> X Y]
        A->B
        (function-transformer
          A-transformer
          B-transformer))
:- [:=> X Y]

(For those new to typing rules, read e :- t as "expression e has schema t" and

condition1
condition2
==========
consequent

as "if condition1 and condition2, then consequent".)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants