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

Generating controllers via ffi templating #4

Open
kfish opened this issue Nov 11, 2013 · 15 comments
Open

Generating controllers via ffi templating #4

kfish opened this issue Nov 11, 2013 · 15 comments

Comments

@kfish
Copy link
Member

kfish commented Nov 11, 2013

In commit 2956a4b (branch example-templating) I'm trying to generate Angular controller code via Fay ffi,

In src/Angular.hs there is:

data StateController a = SC
  { muts :: [(a -> a)]
  , gets :: [Text]
  }

stateController :: StateController a -> a -> Fay ()
stateController = ffi "\
  \ (function($scope) { \
    \ $scope.state = %2; \
    \ var sc = %1; \
    \ \
...

which is called in examples/todo/todo.js:

TodoCtrl = Strict.Angular.stateController(Strict.TodoFay.todoSC,Strict.TodoFay.initialState);

However this fails with:

Error: Argument 'TodoCtrl' is not a function, got Fay$$Monad

The goal is to generate all the Angular code required for a Controller (and later, for a Directive etc.) from a Haskell specification, so fay-angular would need to generate various javascript classes/functions that are structured in the required way, with argument name $scope etc. Any suggestions about where the best place to implement this would be? is it possible from within a fay library, or will it require some modification to the fay compiler itself?

@bergmark
Copy link
Member

This might be an omission in the runtime for serializing the fay monad, i'll look into it!

As a side note, calling the FFI from the strictness wrapper, pretty funky stuff :) But I think it'll work.

@bergmark
Copy link
Member

I added that test case, but I also tested this one which doesn't work, it may be the same issue:

module StrictWrapper where

clog2 :: Int -> Double -> Fay ()
clog2 = ffi "(function () { console.log(%1, %2); })()"

main :: Fay ()
main = (ffi "Strict.StrictWrapper.clog2(234)" :: Double -> Fay ()) 345

It might be the same as faylang/fay#267 too.

Could you try making a smaller reproduction of this please?

@kfish
Copy link
Member Author

kfish commented Nov 18, 2013

The following 3 files are intended to generate a minimal valid angular controller that is initialized via a function call. It fails wtih

Error: Argument 'ConstCtrl' is not a function, got Fay$$Monad

AngularConst.hs (build with "fay --package fay-text AngularConst.hs --strict AngularConst"):

module AngularConst where

import FFI
import Prelude

constController :: Int -> Fay ()
constController = ffi "\
  \ (function($scope) { \
    \ $scope.val = %1; \
    \ console.log($scope.val); \
    \ $scope.getVal = function () { return $scope.val }; \
  \ })"

const.js:

ConstCtrl = Strict.AngularConst.constController(7);

index.html:

<!doctype html>
<html ng-app>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>
    <script src="AngularConst.js"></script>
    <script src="const.js"></script>
  </head>
  <body>
    <h2>Constant</h2>
    <div ng-controller="ConstCtrl">
      <span>Initialized with {{val}}, returns {{getVal()}}</span>
    </div>
  </body>
</html>

The generated code is:

AngularConst.constController = function($p1){
  return new Fay$$$(function(){
    return new Fay$$Monad(Fay$$jsToFay(["unknown"], (function($scope) {  $scope.val = Fay$$fayToJs_int($p1);  console.log($scope.val);  $scope.getVal = function () { return $scope.val };  })));
  });
};

By comparison, a working javascript version of const.js is:

mkConstController = function (val) {
  return (function ($scope) {
    $scope.val = val;
    console.log($scope.val);
    $scope.getVal = function () { return $scope.val };
  })
}

ConstCtrl = mkConstController(7);

@bergmark
Copy link
Member

Try the second commit, it seems to do the trick. I have the same fay module as you:

module AngularConst where

import           FFI
import           Prelude

constController :: Int -> Fay ()
constController = ffi "\
  \ (function($scope) { \
    \ $scope.val = %1; \
    \ console.log($scope.val); \
    \ $scope.getVal = function () { return $scope.val }; \
  \ })"

This html:

<!doctype html>
<html>
  <head>
    <script src="AngularConst.js"></script>
    <script>
var c = Strict.AngularConst.constController(7);
var o = {};
var r = c(o);
    </script>
  </head>
  <body>
  </body>
</html>

Compiling with fay --pretty --Wall --strict AngularConst --library AngularConst.hs --runtime-path js/runtime.js (--runtime-path is just so you don't have to recompile fay when you modify runtime.js)

This logs 7, o.val => 7, o.getVal() => 7

@kfish
Copy link
Member Author

kfish commented Nov 18, 2013

Yes, that does load without errors, and if I modify the generated constController to log when it is called, it gets called.

When loaded with Angular though, the generated code does not seem to be recognized as an Angular controller. The generated code is:

> console.log(ConstCtrl)
function (){
      var fayFunc = fayObj;
      var return_type = args[args.length-1];
      var len = args.length;
      // If some arguments.
      if (len > 1) {
        // Apply to all the arguments.
        fayFunc = Fay$$_(fayFunc,true);
        // TODO: Perhaps we should throw an error when JS
        // passes more arguments than Haskell accepts.

        // Unserialize the JS values to Fay for the Fay callback.
        if (args == "automatic_function")
        {
          for (var i = 0; i < arguments.length; i++) {
            fayFunc = Fay$$fayToJs(["automatic"], Fay$$_(fayFunc(Fay$$jsToFay(["automatic"],arguments[i])),true));
          }
          return fayFunc;
        }

        for (var i = 0, len = len; i < len - 1 && fayFunc instanceof Function; i++) {
          fayFunc = Fay$$_(fayFunc(Fay$$jsToFay(args[i],arguments[i])),true);
        }
        // Finally, serialize the Fay return value back to JS.
        var return_base = return_type[0];
        var return_args = return_type[1];
        // If it's a monadic return value, get the value instead.
        if(return_base == "action") {
          return Fay$$fayToJs(return_args[0],fayFunc.value);
        }
        // Otherwise just serialize the value direct.
        else {
          return Fay$$fayToJs(return_type,fayFunc);
        }
      } else {
        throw new Error("Nullary function?");
      }
    } 

This doesn't look like the function($scope) {..} that Angular is expecting. Also I see no mention of the captured 7, but I don't really understand what is going on :)

@bergmark
Copy link
Member

That's the jsToFay code for functions, it partially applies the arguments it gets to the original fay function you are serializing. It's supposed to be called as a normal function. Can you tell why angular doesn't accept it?

@bergmark
Copy link
Member

c is the same thing in my example by the way. The runtime has to do this wrapping since it doesn't know how many arguments the fay function expects.

@kfish
Copy link
Member Author

kfish commented Nov 18, 2013

My guess is that Angular expects a function argument called exactly "$scope"; if I modify a javascript Angular controller and rename the $scope argument, Angular fails to load the controller.

@btford is this the case, or do you have any suggestions about how we should interface with Angular?

@bergmark
Copy link
Member

Ehm, i'm not sure what to think of this :) http://stackoverflow.com/a/12108723/182603

@bergmark
Copy link
Member

Oh, I forgot about this detail:

Strict.AngularConst.constController(7)()
=>
function ($scope) {  $scope.val = Fay$$fayToJs_int($p1);  console.log($scope.val);  $scope.getVal = function () { return $scope.val };  } 

though i think this won't work in general. I have to think more about serializing actions before I know what the implementation should really be.

@btford
Copy link

btford commented Nov 18, 2013

@kfish Angular infers the name based on the function parameters, unless you add annotations. This is convenient when developing, because it frees you from having to maintain two lists of dependencies for a controller and keep them in sync. However, you probably want to avoid that in a case like this. :)

function MyCtrl ($scope) {
  // ...
}

Is the same as:

function MyCtrl (foo) {
  // $scope aliased to foo
}.$inject = ['$scope'];

More info here: http://docs.angularjs.org/guide/di#dependency-injection_dependency-annotation_-annotation

@ibotty
Copy link

ibotty commented Nov 18, 2013

My guess is that Angular expects a function argument called exactly
"$scope"

that's exactly right. that can be mitigated when using explicit
dependency injection. see "dependency annotation" in
http://docs.angularjs.org/guide/di for two ways to pass them as
strings (which fay should not mangle and which are nice for minification
as well).

@bergmark
Copy link
Member

Thanks for the input! Happy to see that there are other ways of doing it :)

I'm not sure if the other approaches will make much difference, what we have are functions generated dynamically by fay so we can't force structure on it (as in naming arguments or assigning properties to them). But like I said, I need to experiment some more with the serialization to see what it should actually look like.

@kfish you can use what i wrote above for now at least.

@kfish
Copy link
Member Author

kfish commented Nov 18, 2013

@bergmark wow yes, just adding an extra () in const.js works; the serialized function is generated, including the verbatim "$scope" as needed. I'll try to modify the StateController similarly ...

@btford @ibotty thanks for your advice!

@bergmark
Copy link
Member

I thought a bit more about it, this only works here because we have the function as an ffi string. I'm not sure what your plans are, but if you try to create these controllers from normal fay code you will end up with unnamed functions and this won't work. We'd need to be able to tell angular "yes this is really a controller" somehow, or wrap the fay action through js or the ffi before passing it to angular.

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

4 participants