What's so bad about Template Haskell?
Asked Answered
A

6

270

It seems that Template Haskell is often viewed by the Haskell community as an unfortunate convenience. It's hard to put into words exactly what I have observed in this regard, but consider these few examples

I've seen various blog posts where people do pretty neat stuff with Template Haskell, enabling prettier syntax that simply wouldn't be possible in regular Haskell, as well as tremendous boilerplate reduction. So why is it that Template Haskell is looked down upon in this way? What makes it undesirable? Under what circumstances should Template Haskell be avoided, and why?

Ario answered 1/6, 2012 at 20:36 Comment(8)
Voted to Close: belongs on programmers.stackexchange.com - Q&A for professional programmers interested in conceptual questions about software developmentMusician
I disagree with the vote to move; I am asking this question in the same spirit that I asked What's so bad about Lazy I/O? and I expect to see answers of the same fashion. I am open to rewording the question if that would help.Ario
@ErikPhilips Why don't you let the people who frequent this tag decide if it belongs here? It seems like your only interaction with the Haskell community is to put down its questions.Pachyderm
@GabrielGonzalez it obvious with the current question and answer that it does not follow the FAQ under what kind of question should I not ask here: there is no actual problem to be solved. The question has no code specific problem solved and is only conceptual in nature. And based on the question should I avoid template haskell, too it falls into the Stack Overflow is not a Recommendation Engine.Musician
@ErikPhilips The Recommendation Engine aspect is irrelevant to this question, because you'll notice that such is referring to a decision between different tools (e.g. "tell me which language I should use"). In contrast, I'm merely asking for explanation regarding Template Haskell specifically, and the FAQ states "if your motivation is “I would like others to explain [blank] to me”, then you are probably OK." Compare with, for example, GOTO still considered harmful?Ario
I've edited both the title and the concluding sentences to focus the spotlight less on the community's behavior and more on Template Haskell itself. Although I've already accepted an answer, I'd be more than happy to see more answers if there is more ground to cover.Ario
Voting to reopen. Just because it's a higher level question, doesn't mean it's not a good one.Perfectible
Template Haskell turns Haskell into a multi-paradigm langauge (lazy functional + meta programming). I'm glad TH is there - it is useful and some people do really smart things with it (e.g. Geoffrey Mainland's Flask) but personally I like to code just in the functional paradigm.Paravane
A
177

One reason for avoiding Template Haskell is that it as a whole isn't type-safe, at all, thus going against much of "the spirit of Haskell." Here are some examples of this:

  • You have no control over what kind of Haskell AST a piece of TH code will generate, beyond where it will appear; you can have a value of type Exp, but you don't know if it is an expression that represents a [Char] or a (a -> (forall b . b -> c)) or whatever. TH would be more reliable if one could express that a function may only generate expressions of a certain type, or only function declarations, or only data-constructor-matching patterns, etc.
  • You can generate expressions that don't compile. You generated an expression that references a free variable foo that doesn't exist? Tough luck, you'll only see that when actually using your code generator, and only under the circumstances that trigger the generation of that particular code. It is very difficult to unit test, too.

TH is also outright dangerous:

  • Code that runs at compile-time can do arbitrary IO, including launching missiles or stealing your credit card. You don't want to have to look through every cabal package you ever download in search for TH exploits.
  • TH can access "module-private" functions and definitions, completely breaking encapsulation in some cases.

Then there are some problems that make TH functions less fun to use as a library developer:

  • TH code isn't always composable. Let's say someone makes a generator for lenses, and more often than not, that generator will be structured in such a way that it can only be called directly by the "end-user," and not by other TH code, by for example taking a list of type constructors to generate lenses for as the parameter. It is tricky to generate that list in code, while the user only has to write generateLenses [''Foo, ''Bar].
  • Developers don't even know that TH code can be composed. Did you know that you can write forM_ [''Foo, ''Bar] generateLens? Q is just a monad, so you can use all of the usual functions on it. Some people don't know this, and because of that, they create multiple overloaded versions of essentially the same functions with the same functionality, and these functions lead to a certain bloat effect. Also, most people write their generators in the Q monad even when they don't have to, which is like writing bla :: IO Int; bla = return 3; you are giving a function more "environment" than it needs, and clients of the function are required to provide that environment as an effect of that.

Finally, there are some things that make TH functions less fun to use as an end-user:

  • Opacity. When a TH function has type Q Dec, it can generate absolutely anything at the top-level of a module, and you have absolutely no control over what will be generated.
  • Monolithism. You can't control how much a TH function generates unless the developer allows it; if you find a function that generates a database interface and a JSON serialization interface, you can't say "No, I only want the database interface, thanks; I'll roll my own JSON interface"
  • Run time. TH code takes a relatively long time to run. The code is interpreted anew every time a file is compiled, and often, a ton of packages are required by the running TH code, that have to be loaded. This slows down compile time considerably.
Aminaamine answered 1/6, 2012 at 20:57 Comment(10)
Add to this the fact that template-haskell has historically been very poorly documented. (Although I just looked again and it seems that things are at least marginally better now.) Also, to understand template-haskell, you essentially have to understand the Haskell language grammar, which imposes a certain amount of complexity (it's no Scheme). These two things contributed to me intentionally not bothering to understand TH when I was a Haskell beginner.Wheezy
Don't forget that the use of Template Haskell suddently means the order of declarations matters! TH just is not integrated as tightly as one might hope given the smooth polish of Haskell (be it 1.4, '98, 2010 or even Glasgow).Vernonvernor
You can reason about Haskell without too much difficulty, there's no such guarantee about Template Haskell.Bioclimatology
And what happened to Oleg's promise of a type-safe alternative to TH? I'm referring to his work based off of his "Finally Tagless, Partially Evaluated" paper and more of his notes here. It looked so promising when they announced it and then I never heard another word about it.Pachyderm
There is a proposal to fix some problems with TH.Servomechanism
re: "TH is dangerous because it can arbitrary IO at compile time". Can't cabal hooks already do arbitrary IO, independent of what happens during the build?Festination
If TH isn't type-safe, why does the wiki tell it is type-safe? wiki.haskell.org/Template_HaskellHuxley
TH IO hardly sounds dangerous. When you run a program you can also do IO. How often do you compile something and not run it? You should be making sure the packages you use are safe anyway.Garfield
@Garfield The difference is that compromised dependencies only contaminate the build products. Compromised TH depencies can turns /GHC/ into an attack vector. Having to worry about an exploited compiler is an absolute security nightmare: it turns every developer machine and build server into a bot. Sounds not too bad at first sight since many organizations isolate them already. But a compromised developer machine can still cause a lot of havoc.Blacksnake
@Blacksnake What developer machine is compiling software and not running the compiled result?Garfield
M
54

This is solely my own opinion.

  • It's ugly to use. $(fooBar ''Asdf) just does not look nice. Superficial, sure, but it contributes.

  • It's even uglier to write. Quoting works sometimes, but a lot of the time you have to do manual AST grafting and plumbing. The API is big and unwieldy, there's always a lot of cases you don't care about but still need to dispatch, and the cases you do care about tend to be present in multiple similar but not identical forms (data vs. newtype, record-style vs. normal constructors, and so on). It's boring and repetitive to write and complicated enough to not be mechanical. The reform proposal addresses some of this (making quotes more widely applicable).

  • The stage restriction is hell. Not being able to splice functions defined in the same module is the smaller part of it: the other consequence is that if you have a top-level splice, everything after it in the module will be out of scope to anything before it. Other languages with this property (C, C++) make it workable by allowing you to forward declare things, but Haskell doesn't. If you need cyclic references between spliced declarations or their dependencies and dependents, you're usually just screwed.

  • It's undisciplined. What I mean by this is that most of the time when you express an abstraction, there is some kind of principle or concept behind that abstraction. For many abstractions, the principle behind them can be expressed in their types. For type classes, you can often formulate laws which instances should obey and clients can assume. If you use GHC's new generics feature to abstract the form of an instance declaration over any datatype (within bounds), you get to say "for sum types, it works like this, for product types, it works like that". Template Haskell, on the other hand, is just macros. It's not abstraction at the level of ideas, but abstraction at the level of ASTs, which is better, but only modestly, than abstraction at the level of plain text.*

  • It ties you to GHC. In theory another compiler could implement it, but in practice I doubt this will ever happen. (This is in contrast to various type system extensions which, though they might only be implemented by GHC at the moment, I could easily imagine being adopted by other compilers down the road and eventually standardized.)

  • The API isn't stable. When new language features are added to GHC and the template-haskell package is updated to support them, this often involves backwards-incompatible changes to the TH datatypes. If you want your TH code to be compatible with more than just one version of GHC you need to be very careful and possibly use CPP.

  • There's a general principle that you should use the right tool for the job and the smallest one that will suffice, and in that analogy Template Haskell is something like this. If there's a way to do it that's not Template Haskell, it's generally preferable.

The advantage of Template Haskell is that you can do things with it that you couldn't do any other way, and it's a big one. Most of the time the things TH is used for could otherwise only be done if they were implemented directly as compiler features. TH is extremely beneficial to have both because it lets you do these things, and because it lets you prototype potential compiler extensions in a much more lightweight and reusable way (see the various lens packages, for example).

To summarize why I think there are negative feelings towards Template Haskell: It solves a lot of problems, but for any given problem that it solves, it feels like there should be a better, more elegant, disciplined solution better suited to solving that problem, one which doesn't solve the problem by automatically generating the boilerplate, but by removing the need to have the boilerplate.

* Though I often feel that CPP has a better power-to-weight ratio for those problems that it can solve.

EDIT 23-04-14: What I was frequently trying to get at in the above, and have only recently gotten at exactly, is that there's an important distinction between abstraction and deduplication. Proper abstraction often results in deduplication as a side effect, and duplication is often a telltale sign of inadequate abstraction, but that's not why it's valuable. Proper abstraction is what makes code correct, comprehensible, and maintainable. Deduplication only makes it shorter. Template Haskell, like macros in general, is a tool for deduplication.

Maurizio answered 3/6, 2012 at 15:16 Comment(3)
"CPP has a better power-to-weight ratio for those problems that it can solve". Indeed. And in C99 it can solve whatever you want. Consider these: COS, Chaos. I also don't udnerstand why people think AST generation is better. It's just more ambiguity and less orthogonal to other language featuresJaine
Your reform proposal link is dead now.Phidippides
@eyelash, I updated the link.Ergot
G
31

I'd like to address a few of the points dflemstr brings up.

I don't find the fact that you can't typecheck TH to be that worrying. Why? Because even if there is an error, it will still be compile time. I'm not sure if this strengthens my argument, but this is similar in spirit to the errors that you receive when using templates in C++. I think these errors are more understandable than C++'s errors though, as you'll get a pretty printed version of the generated code.

If a TH expression / quasi-quoter does something that's so advanced that tricky corners can hide, then perhaps it's ill-advised?

I break this rule quite a bit with quasi-quoters I've been working on lately (using haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples . I know this introduces some bugs such as not being able to splice in the generalized list comprehensions. However, I think that there's a good chance that some of the ideas in http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal will end up in the compiler. Until then, the libraries for parsing Haskell to TH trees are a nearly perfect approximation.

Regarding compilation speed / dependencies, we can use the "zeroth" package to inline the generated code. This is at least nice for the users of a given library, but we can't do much better for the case of editing the library. Can TH dependencies bloat generated binaries? I thought it left out everything that's not referenced by the compiled code.

The staging restriction / splitting of compilation steps of the Haskell module does suck.

RE Opacity: This is the same for any library function you call. You have no control over what Data.List.groupBy will do. You just have a reasonable "guarantee" / convention that the version numbers tell you something about the compatibility. It is somewhat of a different matter of change when.

This is where using zeroth pays off - you're already versioning the generated files - so you'll always know when the form of the generated code has changed. Looking at the diffs might be a bit gnarly, though, for large amounts of generated code, so that's one place where a better developer interface would be handy.

RE Monolithism: You can certainly post-process the results of a TH expression, using your own compile-time code. It wouldn't be very much code to filter on top-level declaration type / name. Heck, you could imagine writing a function that does this generically. For modifying / de-monolithisizing quasiquoters, you can pattern match on "QuasiQuoter" and extract out the transformations used, or make a new one in terms of the old.

Grum answered 2/6, 2012 at 2:54 Comment(7)
Regarding Opacity/Monolithism: You can of course walk through a [Dec] and remove stuff that you don't want, but let's say that the function reads an external definition file when generating the JSON interface. Just because you don't use that Dec doesn't make the generator stop looking for the definition file, making the compile fail. For that reason, it would be nice to have a more restrictive version of the Q monad that'd let you generate new names (and such things) but not allow IO, so that, as you say, one could filter its results/do other compositions.Aminaamine
I agree, there should be a non-IO version of Q / quotation! This would also help resolve - #7107808 . Once you ensure that the compile-time code doesn't do anything unsafe, it seems like you could just run the safety checker on the resulting code, and make sure it doesn't reference private stuff.Grum
Did you ever write up your counterargument? Still waiting for your promised link. For those looking for the old contents of this answer: stackoverflow.com/revisions/10859441/1 , and for those that can view deleted content: stackoverflow.com/revisions/10913718/6Ario
Yeah, I never got around to it. While writing my more detailed counterarguments I realized it would be a good way to introduce some of my TH libraries, and got distracted working on those. Thanks for putting those links there! I've re-instated my comment for ease / posterity. Since then I've further developed my views on whats good about TH, and what should change, so at some point I will make a post about that!Grum
is there a more up-to-date version of zeroth than the one on hackage? That one (and the darcs repo) were last updated in 2009. Both copies don't build with a current ghc (7.6).Hindoo
@Hindoo tgeeky was working on fixing it up, but I don't think he finished: github.com/technogeeky/zeroth If noone does it / make something for this, I'll get around to it eventually.Grum
"This is the same for any library function you call. You have no control over what Data.List.groupBy will do." Unless unsafePerformIO is used, Data.List.groupBy is limited to returning a list of lists. Template Haskell isn't.Blacksnake
G
15

This answer is in response to the issues brought up by illissius, point by point:

  • It's ugly to use. $(fooBar ''Asdf) just does not look nice. Superficial, sure, but it contributes.

I agree. I feel like $( ) was chosen to look like it was part of the language - using the familiar symbol pallet of Haskell. However, that's exactly what you /don't/ want in the symbols used for your macro splicing. They definitely blend in too much, and this cosmetic aspect is quite important. I like the look of {{ }} for splices, because they are quite visually distinct.

  • It's even uglier to write. Quoting works sometimes, but a lot of the time you have to do manual AST grafting and plumbing. The [API][1] is big and unwieldy, there's always a lot of cases you don't care about but still need to dispatch, and the cases you do care about tend to be present in multiple similar but not identical forms (data vs. newtype, record-style vs. normal constructors, and so on). It's boring and repetitive to write and complicated enough to not be mechanical. The [reform proposal][2] addresses some of this (making quotes more widely applicable).

I also agree with this, however, as some of the comments in "New Directions for TH" observe, the lack of good out-of-the-box AST quoting is not a critical flaw. In this WIP package, I seek to address these problems in library form: https://github.com/mgsloan/quasi-extras . So far I allow splicing in a few more places than usual and can pattern match on ASTs.

  • The stage restriction is hell. Not being able to splice functions defined in the same module is the smaller part of it: the other consequence is that if you have a top-level splice, everything after it in the module will be out of scope to anything before it. Other languages with this property (C, C++) make it workable by allowing you to forward declare things, but Haskell doesn't. If you need cyclic references between spliced declarations or their dependencies and dependents, you're usually just screwed.

I've run into the issue of cyclic TH definitions being impossible before... It's quite annoying. There is a solution, but it's ugly - wrap the things involved in the cyclic dependency in a TH expression that combines all of the generated declarations. One of these declarations generators could just be a quasi-quoter that accepts Haskell code.

  • It's unprincipled. What I mean by this is that most of the time when you express an abstraction, there is some kind of principle or concept behind that abstraction. For many abstractions, the principle behind them can be expressed in their types. When you define a type class, you can often formulate laws which instances should obey and clients can assume. If you use GHC's [new generics feature][3] to abstract the form of an instance declaration over any datatype (within bounds), you get to say "for sum types, it works like this, for product types, it works like that". But Template Haskell is just dumb macros. It's not abstraction at the level of ideas, but abstraction at the level of ASTs, which is better, but only modestly, than abstraction at the level of plain text.

It's only unprincipled if you do unprincipled things with it. The only difference is that with the compiler implemented mechanisms for abstraction, you have more confidence that the abstraction isn't leaky. Perhaps democratizing language design does sound a bit scary! Creators of TH libraries need to document well and clearly define the meaning and results of the tools they provide. A good example of principled TH is the derive package: http://hackage.haskell.org/package/derive - it uses a DSL such that the example of many of the derivations /specifies/ the actual derivation.

  • It ties you to GHC. In theory another compiler could implement it, but in practice I doubt this will ever happen. (This is in contrast to various type system extensions which, though they might only be implemented by GHC at the moment, I could easily imagine being adopted by other compilers down the road and eventually standardized.)

That's a pretty good point - the TH API is pretty big and clunky. Re-implementing it seems like it could be tough. However, there are only really only a few ways to slice the problem of representing Haskell ASTs. I imagine that copying the TH ADTs, and writing a converter to the internal AST representation would get you a good deal of the way there. This would be equivalent to the (not insignificant) effort of creating haskell-src-meta. It could also be simply re-implemented by pretty printing the TH AST and using the compiler's internal parser.

While I could be wrong, I don't see TH as being that complicated of a compiler extension, from an implementation perspective. This is actually one of the benefits of "keeping it simple" and not having the fundamental layer be some theoretically appealing, statically verifiable templating system.

  • The API isn't stable. When new language features are added to GHC and the template-haskell package is updated to support them, this often involves backwards-incompatible changes to the TH datatypes. If you want your TH code to be compatible with more than just one version of GHC you need to be very careful and possibly use CPP.

This is also a good point, but somewhat dramaticized. While there have been API additions lately, they haven't been extensively breakage inducing. Also, I think that with the superior AST quoting I mentioned earlier, the API that actually needs to be used can be very substantially reduced. If no construction / matching needs distinct functions, and are instead expressed as literals, then most of the API disappears. Moreover, the code you write would port more easily to AST representations for languages similar to Haskell.


In summary, I think that TH is a powerful, semi-neglected tool. Less hate could lead to a more lively eco-system of libraries, encouraging the implementation of more language feature prototypes. It's been observed that TH is an overpowered tool, that can let you /do/ almost anything. Anarchy! Well, it's my opinion that this power can allow you to overcome most of its limitations, and construct systems capable of quite principled meta-programming approaches. It's worth the usage of ugly hacks to simulate the "proper" implementation, as this way the design of the "proper" implementation will gradually become clear.

In my personal ideal version of nirvana, much of the language would actually move out of the compiler, into libraries of these variety. The fact that the features are implemented as libraries does not heavily influence their ability to faithfully abstract.

What's the typical Haskell answer to boilerplate code? Abstraction. What're our favorite abstractions? Functions and typeclasses!

Typeclasses let us define a set of methods, that can then be used in all manner of functions generic on that class. However, other than this, the only way classes help avoid boilerplate is by offering "default definitions". Now here is an example of an unprincipled feature!

  • Minimal binding sets are not declarable / compiler checkable. This could lead to inadvertent definitions that yield bottom due to mutual recursion.

  • Despite the great convenience and power this would yield, you cannot specify superclass defaults, due to orphan instances http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ These would let us fix the numeric hierarchy gracefully!

  • Going after TH-like capabilities for method defaults led to http://www.haskell.org/haskellwiki/GHC.Generics . While this is cool stuff, my only experience debugging code using these generics was nigh-impossible, due to the size of the type induced for and ADT as complicated as an AST. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c

    In other words, this went after the features provided by TH, but it had to lift an entire domain of the language, the construction language, into a type system representation. While I can see it working well for your common problem, for complex ones, it seems prone to yielding a pile of symbols far more terrifying than TH hackery.

    TH gives you value-level compile-time computation of the output code, whereas generics forces you to lift the pattern matching / recursion part of the code into the type system. While this does restrict the user in a few fairly useful ways, I don't think the complexity is worth it.

I think that the rejection of TH and lisp-like metaprogramming led to the preference towards things like method-defaults instead of more flexible, macro-expansion like declarations of instances. The discipline of avoiding things that could lead to unforseen results is wise, however, we should not ignore that Haskell's capable type system allows for more reliable metaprogramming than in many other environments (by checking the generated code).

Grum answered 6/6, 2012 at 11:55 Comment(5)
This answer doesn't stand on its own very well: you're making a bunch of references to another answer that I have to go and find before I can read yours properly.Airboat
It's true. I tried to make it clear what was being talked about despite. Perhaps I'll edit to inline illisuis's points.Grum
I should say that maybe "unprincipled" is a stronger word than I should have used, with some connotations that I didn't intend - it's certainly not unscrupulous! That bullet point was the one I had the most trouble with, because I have this feeling or unformed idea in my head and trouble putting it into words, and "unprincipled" was one of the words I grasped which was somewhere in the vicinity. "Disciplined" is probably closer. Not having a clear and succint formulation I went for some examples instead. If someone can explain to me more clearly what I probably meant, I would appreciate it!Maurizio
(Also I think you might have switched up the staging restriction and ugly-to-write quotes.)Maurizio
Doh! Thanks for pointing that out! Fixed. Yes, undisciplined would probably be a better way of putting it. I think that the distinction here is really between "built-in", and therefore "well understood" abstraction mechanisms, versus "ad-hoc", or user-defined abstraction mechanisms. I suppose what I mean is that you could conceivably define a TH library which implemented something quite similar to typeclass dispatch (albeit at compile time not runtime)Grum
S
8

One rather pragmatic problem with Template Haskell is that it only works when GHC's bytecode interpreter is available, which is not the case on all architectures. So if your program uses Template Haskell or relies on libraries that use it, it will not run on machines with an ARM, MIPS, S390 or PowerPC CPU.

This is relevant in practice: git-annex is a tool written in Haskell that makes sense to run on machines worrying about storage, such machines often have non-i386-CPUs. Personally, I run git-annex on a NSLU 2 (32 MB of RAM, 266MHz CPU; did you know Haskell works fine on such hardware?) If it would use Template Haskell, this is not possible.

(The situation about GHC on ARM is improving these days a lot and I think 7.4.2 even works, but the point still stands).

Spiegel answered 7/6, 2012 at 10:14 Comment(4)
“Template Haskell relies on GHC's built-in bytecode compiler and interpreter to run the splice expressions.” – haskell.org/ghc/docs/7.6.2/html/users_guide/…Spiegel
ah, I that -- no, TH won't work without the bytecode interpreter, but that's distinct from (though relevant to) ghci. I wouldn't be surprised if there was a perfect relation between availability of ghci and availability of the bytecode interpreter, given that ghci does depend on the bytecode interpreter, but the problem there is the lack of the bytecode interpreter, not the lack of ghci specifically.Kristlekristo
(incidentally, ghci has amusingly poor support for interactive use of TH. try ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )')Kristlekristo
Ok, with ghci I was referring to the ability of GHC to interpret (instead of compile) code, independent of whether the code comes interactively in the ghci binary, or from TH splice.Spiegel
L
7

Why is TH bad? For me, it comes down to this:

If you need to produce so much repetitive code that you find yourself trying to use TH to auto-generate it, you're doing it wrong!

Think about it. Half the appeal of Haskell is that its high-level design allows you to avoid huge amounts of useless boilerplate code that you have to write in other languages. If you need compile-time code generation, you're basically saying that either your language or your application design has failed you. And we programmers don't like to fail.

Sometimes, of course, it's necessary. But sometimes you can avoid needing TH by just being a bit more clever with your designs.

(The other thing is that TH is quite low-level. There's no grand high-level design; a lot of GHC's internal implementation details are exposed. And that makes the API prone to change...)

Lodgment answered 5/1, 2014 at 20:46 Comment(2)
I don't think it means that your language or application has failed you, especially if we consider QuasiQuotes. There are always trade-offs when it comes to syntax. Some syntax better describes a certain domain, so you want to be able to switch to another syntax sometimes. QuasiQuotes allow you to elegantly switch between syntaxes. This is very powerful and is used by Yesod and other apps. Being able to write HTML generation code using syntax that feels like HTML is an amazing feature.Trivial
@Trivial Yeah, quasi-quoting is quite nice, and I suppose slightly orthogonal to TH. (Obviously it's implemented on top of TH.) I was thinking more about using TH to generate class instances, or build functions of multiple arities, or something like that.Lodgment

© 2022 - 2024 — McMap. All rights reserved.