eta
eta copied to clipboard
[eta] Annotations and ADT foreign export
There are many frameworks that depend on annotations. I will be using Spring Boot as an example.
Spring Boot is a widespread Java framework that does not enforce any kind of structure, while being able to use all the Spring Boot modules to make application development easier.
These modules include interoperability with:
- CassandraDB
- CouchDB
- Kafka
- Many caching systems
- Queueing systems
- And many more
The problem here is, Spring Boot is mainly annotation driven. And while we are able to do dependency injection using a custom environment (like a ReaderT
), other features of the framework are not accessible because of its hard dependency on annotations.
Of course, one can use the Java Persistence API (JPA) to handle the creation and modification of database entries.
Example application
This is an example of a very simple Spring Boot application that handles persistence and routing:
I'll be using Scala as the example language just for the sake of brevity, as it is less verbose and it is directly translatable to Java.
Application.scala
@SpringBootApplication
class Application { }
def main(args: Array[String]) = {
SpringApplication.run(classOf[Application], args)
}
This is the entry point of our application. We are passing Application.class
to the run
method so Spring can operate on it using reflection.
I can imagine doing this easily in Eta:
@SpringBootApplication
data Application = Application @eta.myexample.Application
main :: IO ()
main = do
args <- getArgs
runSpringApplication (getClass (Proxy :: Proxy Application)) args
Post.scala
@Entity
class Post {
@Id
@GeneratedValue
var id: Long = null
var title: String = null
var content: String = null
}
This class defines an entity that will be stored in the Post
table.
I think it would be quite tricky to do so, unless we force the exported ADTs to be records:
@Entity
data Post = Post
{ id :: @Id @GeneratedValue (Mutable Long)
, title :: Mutable String
, content :: Mutable String
}
(Idea: non-records fields could be exported as _1
, _2
, _n
)
PostRepository.scala
trait PostRepository extends JpaRepository[Post, Long] { }
This is just an interface that allows us to use some methods to access our database:
data PostRepository = PostRepository @foo.PostRepository
type instance Inherits PostRepository = '[JpaRepository Post Long]
(Note: I'm not sure if this is the correct way to do so)
PostController.scala
@Controller
@RequestMapping("/")
class PostController {
@Autowired
var postRepository: PostRepository = null
@GetMapping("/")
def posts(model: Model): String = {
val posts = postRepository.findAll()
model.addAttribute("posts", posts)
"postList"
}
@PostMapping("/")
def addToPostsList(post: Post): String = {
postRepository.save(post)
"redirect:/"
}
}
This is the biggest part, the controller that handles all the requests and uses the other classes
@Controller
@RequestMapping "/"
data PostController = PostController
{ postRepository :: @Autowired (Mutable PostRepository)
}
@GetMapping "/"
posts :: Model -> Java PostController String
posts model = do
posts <- postRepository <.> findAll
model <.> addAttribute "posts" posts
return "postList"
@PostMapping "/"
addToPostsList :: Post -> Java PostController String
addToPostsList post = do
postRepository <.> save post
return "redirect:/"
It looks quite feasible to me.
Note: I'm not an expert in the Eta export features. Although I'm a Haskeller, I haven't used Eta too much :)
**Note 2: This are just snippets of code, if the whole project is needed, you can find it in this repository, this commit **
Thanks for the beautiful write-up, @NickSeagull!
I agree, this looks quite feasible. I made the following edits to your original issue:
-
I moved the annotations into the types instead of the identifiers. Since types describe a value, we can treat annotations as types that just add more context, but are effectively treated as comments by the typechecker. So
@Autowired PostRepository ~ PostRepository
as far as the typechecker is concerned. Only the foreign export generator in the compiler concerns itself with the annotations. -
I added a
Mutable
type constructor in front of all the potentially mutable fields. Field with aMutable
identifier generate record selectors with slightly different type signatures:title :: Mutable String
would generate
title :: Post -> IO String
instead of the usual
title :: Post -> String
to ensure proper ordering of effects. I still need to think over how to work out the syntax for updating such a mutable field in a record, as the existing record syntax won't cut it. Ideas?
-
I got rid of the
foreign export data
because the fact that you're adding annotations to a data declaration automatically indicates that you're intending to export it (there's no other reason to do so). No need for extra typing.We're eventually gonna get rid of
foreign export
keywords altogether and make something more clean. We were originally trying to keep in tune with GHC, but we've already made so many syntactic changes that it doesn't make sense to stick with verbose syntax. -
I updated your code to the new FFI syntax which is a lot less scary.
I might start working on this, what would be the first steps to work on this @rahulmutt ?
Also, could you assign this to me please? :smile:
Also, I saw that the Lexer.x
file has a Java annotation declaration. Is that related?
@NickSeagull Yes, the java annotation declaration was made keeping in mind that we'd eventually support annotations for exports. Right now that is used for parsing the @
notation for JWTs, so you can re-use it when making parsing rules for annotations.
The general idea of how to go about implementing this:
- Annotations should not exist at the
Core
level, since annotations don't affect the code-generation process. Annotations should exist at theHsSyn
level and should disappear as soon as the foreign export generator (which lives inside the desugarer inETA.DeSugar.DsForeigns
). The first part of this issue is updating theHsSyn
syntax tree to store the annotations to make them available to the export generator. - The lexer or parser should be modified to store consecutive annotations into a group and as soon as the next binding definition comes up, stash the stored consecutive annotations into the binding. This is different from how Haskell normally handles things like
{-# INLINE [binder] #-}
because in that case, the[binder]
is specified so we know what the annotation applies to. In this case, we only know it will apply to the next binding definition. - And I take back what I said above - we will have a special annotation
@EXPORT()
that marks the function to be exported. TheEXPORT
annotation will have parameters equivalent to the "export string" part of normalforeign export java
declarations so that you can specify how it is exported. - The way you parse foreign exports now changes - instead of looking for the key words
foreign export java ...
you look for an annotation sequence that starts with@EXPORT
and parse the successive binding to get the type signature.
By the way you should parse annotations exactly like Java does (keeps things consistent): https://docs.oracle.com/javase/specs/jls/se7/html/jls-18.html
Annotations should not exist at the Core level, since annotations don't affect the code-generation process. Annotations should exist at the HsSyn level and should disappear as soon as the foreign export generator (which lives inside the desugarer in ETA.DeSugar.DsForeigns). The first part of this issue is updating the HsSyn syntax tree to store the annotations to make them available to the export generator.
Disregard this entire comment.
Because annotations are only relevant for foreign exports, you need only extend the ForeignExport
decl:
https://github.com/typelead/eta/blob/master/compiler/ETA/HsSyn/HsDecls.hs#L1331-L1334
You can look into the ForeignExport
type and add a field to store Java annotations.
Great, thanks! :smile:
I've started working on this.
I've implemented:
- Support for annotations in
codec-jvm
- Allow the code below to parse:
@Controller
@RequestMapping "/"
data PostController = PostController
{ @Autowired postRepository :: PostRepository
}
@GetMapping "/"
posts :: Model -> Java PostController String
posts model = do
posts <- postRepository <.> findAll
model <.> addAttribute "posts" posts
return "postList"
@PostMapping "/"
addToPostsList :: Post -> Java PostController String
addToPostsList post = do
postRepository <.> save post
return "redirect:/"
Remaining steps:
- Implement support for mutable fields, taking inspiration from the GHC Proposal. The implementation will be done in a way that is not FFI-specific and can be used for normal ADTs too.
- Fully flesh out the parameters that @Export can take and figure out all the different configuration options to expose to the user.
- Send the annotation information down the compilation pipeline and stash them in the classfile.
Any progress on this? Looking forward to it.
No unfortunately. Thanks for showing interest though, I'll pin this issue to indicate that it has priority.