3

I write a runtime type checker in and for Javascript and have trouble to type fix:

fix :: (a -> a) -> a
fix f = ...

fix (\rec n -> if n == 0 then 1 else n * rec (n-1)) 5 -- 120

The factorial function passed to fix has the simplified type (Int -> Int) -> Int -> Int.

When I try to reproduce the expression in Javascript, my type checker fails due to the invalid constraint Int ~ Int -> Int.

fix (const "hallo") also fails and the type checker complaints that it cannot construct an infinite type (negative occurs check).

With other combinators my unification results are consistent with Haskell's.

Is my unifiction algorithm probably wrong or can fix only be typed in non-strict environments?

[EDIT]

My implementation of fix in Javascript is const fix = f => f(f).

duplode
  • 31,361
  • 7
  • 69
  • 130
  • 3
    I don't see how typing has anything to do with strictness. This sounds like a bug in your type checker. – Li-yao Xia Jan 25 '18 at 16:16
  • @Li-yaoXia Yes, you're right. `(a -> a) -> a` applied to `Num t => (t -> t) -> t -> t` yields the constraint `a ~ t -> t` and hence `Num t => t -> t`. It's a bug. –  Jan 25 '18 at 16:55
  • 1
    @Li-yaoXia I have to eta expand `fix` in Javascript and consequently have to type it `fix :: ((a -> a) -> a -> a) -> a -> a`. So strictness affects types but in a different way than I thought. This is exciting! –  Jan 25 '18 at 17:50
  • 1
    Another keyword relevant to recursion in strict languages is "value restriction", e.g., http://mlton.org/ValueRestriction – Li-yao Xia Jan 25 '18 at 20:35

2 Answers2

5

It's a bug in the type checker.

It is true though that the naive Haskell definition of fix does not terminate in Javascript:

> fix = (f) => f(fix(f))
> factf = (f) => (n) => (n === 0) ? 1 : n * f(n - 1)
> fact = fix(factf) // stack overflow

You'd have to use an eta-expanded definition in order to the prevent looping evaluation of fix(f):

> fix = (f) => (a) => f(fix(f), a)
> factf = (f, a) => (a == 0) ? 1 : a * f(a - 1)
> fact = fix(factf)
> fact(10) // prints 3628800
András Kovács
  • 29,038
  • 3
  • 45
  • 94
  • Thanks. Is `fix = (f) => f(fix(f))` the translation of `fix f = let x = f x in x`? I actually declared `fix = f => f(f)`, which makes `f(f) (n - 1)` necessary as recursive call. –  Jan 25 '18 at 16:51
  • I guess I got it: `fix = f => f(f)` would be an infinite type. So `fix` must be declared the way you did in order to match `(a -> a) -> a`. –  Jan 25 '18 at 17:01
  • I indeed used the wrong version of `fix`. When I implement it as your proposed `fix = f => x => f(fix(f)) (x))` and type it explicitly with `((a -> a) -> a -> a) -> a -> a`, it actually works. My type checker is just fine. And as a side effect I finally understood `fix` in Haskell. You got me on the right track. Thanks again! –  Jan 25 '18 at 17:45
  • @ftor *"I actually declared `fix = f => f(f)`"* I understand the downvotes to your Q now. this ***really*** should have been included in the question! :) (FYI that definition is **U combinator**, not **Y**). – Will Ness Jan 25 '18 at 20:07
  • 1
    @WillNess I didn't realize this connection when I posted the question. Anyway, I don't criticize the downvoters. The difference between `U` and `Y` is now clear to me. It was really helpful to understand their types. –  Jan 25 '18 at 20:21
  • For an eta-expanded `fix` the type sig `((a -> a) -> a -> a) -> a -> a` is actually not quite accurate. I tried to implement a tail recursive `fact` with an accumulator and it didn't type check. The proper type is `((a -> b) -> a -> b) -> a -> b`. –  Jan 26 '18 at 14:18
  • @ftor that's strange, as `fix` creates chains of compositions, `fix g = g . g . g . g . ...` and `(a->b) . (a->b)` wouldn't typecheck. maybe you ended up doing some equivalent of a monadic, *extended* composition: `(g <=< g <=< g <=< ...)` so the types aligned as `(a->m a) <=< (a->m a) <=< ...`... – Will Ness Feb 01 '18 at 10:29
  • @WillNess When I check `fix f a = f (fix f) a`'s type in GHCi it is `((t1 -> t) -> t1 -> t) -> t1 -> t` too. Is this the wrong implementation or just a weird GHCi behavior? –  Feb 01 '18 at 15:03
  • right, I as wrong. `((a -> b) -> (a -> b)) -> (a -> b)` is totally fine, matches the `(t -> t) -> t` type for the chained compositions. thanks! – Will Ness Feb 01 '18 at 15:20
1

So as it turns out, you tried to implement U combinator, which is not a fixed-point combinator.

Whereas the fixed-point Y combinator _Y g = g (_Y g) enables us to write

 _Y (\r x -> if x==0 then 1 else x * r (x-1)) 5     -- _Y g => r = _Y g
 -- 120

with _U g = g (g) we'd have to write

 _U (\f x -> if x==0 then 1 else x * f f (x-1)) 5   
                                            -- _U g => f = g, f f == _U g

As you've discovered, this _U has no type in Haskell. On the one hand g is a function, g :: a -> b; on the other it receives itself as its first argument, so it demands a ~ a -> b for any types a and b.

Substituting a -> b for a in a -> b right away leads to infinite recursion (cf. "occurs check"), because (a -> b) -> b still has that a which needs to be replaced with a -> b; ad infinitum.

A solution could be to introduce a recursive type, turning a ~ a -> b into R = R -> b i.e.

 newtype U b = U {app :: U b -> b}      -- app :: U b -> U b -> b

so we can define

 _U :: (U b -> b) -> b
 _U g = g (U g)                         -- g :: U b -> b 
   -- = app (U g) (U g)
   -- = join app (U g)
   -- = (join app . U) g                -- (**)

which now has a type; and use it as

 _U (\f x -> if x==0 then 1 else x * app f f (x-1)) 5
                                        -- _U g  =>  f = U g, 
 -- 120                                 -- app f f = g (U g) == _U g

edit: And if you want to implement the Y combinator as a non-recursive definition, then

 _U (\f x -> if x==0 then 1 else x * (app f f) (x-1))
=                                    -------.-               -- abstraction
 _U (\f -> (\r x -> if x==0 then 1 else x * r (x-1)) (app f f))
=          -------.---------------------------------         -- abstraction
 (\g -> _U (\f -> g (app f f))) (\r x -> if x==0 then 1 else x * r (x-1))
=                                                            --  r = app f f 
 _Yn                            (\r x -> if x==0 then 1 else x * r (x-1))

so

 _Yn :: (b -> b) -> b
 _Yn g = _U (\f -> g (app f f))         -- Y, non-recursively

does the job:

 _Yn (\r x -> if x==0 then 1 else x * r (x-1)) 5
 -- 120

(later update:) Or, point-freer,

 _Yn g = _U (\f -> g (app f f))
       = _U (\f -> g (join app f))
       = _U (\f -> (g . join app) f)
       = _U (g . join app)
       = _U ( (. join app) g )

Thus

 _Yn :: (b -> b) -> b
 _Yn = _U . (. join app)                -- and, using (**)
     = join app . U . (. join app)
Will Ness
  • 62,652
  • 8
  • 86
  • 167
  • I am curious if this newtype along with `U`'s implementation in Javascript satisfies my type checker. I use Scott to encode ADTs, hence my type will be less concise. Thank you Will! –  Jan 26 '18 at 13:21