6 ms·
You can do functional programming without garbage collection. The problem is social: few people in the functional programming community are interested in substr
by catnaroek 10y ago
You can do functional programming without garbage collection. The problem is social: few people in the functional programming community are interested in substructural types.
- bogomipz 10y agoCan you elaborate on how a FP language might be implemented without a garbage collector? How do things like closure/lambdas wok without GC? I tried to lookup "substructural types" but wasn't able to find anything.
- catnaroek 10y ago> I tried to lookup "substructural types" but wasn't able to find anything. A substructural type system lets you express constraints on how many times a value can be used. More precisely: (0) An object of an affine type cannot be used more than once. This is enforced by not allowing the same object to exist in multiple environments. Affinely typed objects can be moved (in the Rust sense) from one environment to another, but as soon as this happens, the object ceases to exist in the original environment. Affinely typed objects can be reclaimed as soon as they are first used (since they can't be used again) or go out of scope (since no other scope has access to them), so no dynamic garbage collection is necessary for them. (1) An object of a relevant type has to be used at least once. This is enforced by requiring the result of every computation to be fed into some other computation. In lazy languages, relevant types can be used for strictness optimizations: if the result of a computation is guaranteed to be used, then computing the result upfront eliminates the overhead of creating lazy suspensions. (2) An object of a linear type has to be used exactly once. A linear type is just a type that is both affine and relevant, so everything mentioned above applies. > How do things like closure/lambdas wok without GC? A closure has its own environment. With affine types, when a closure captures an object from the outer context, the object ceases to exist in the outer environment. This is how move closures already work in Rust. --- A digression: This is why I find GHC's plethora of type system extensions utterly disappointing. They marginally increase Haskell's expressiveness in areas where the language is already very expressive, but they don't address the language's limitations w.r.t. resource management. For instance, Haskell is incapable of statically enforcing that file handles won't leak.
- tome 10y ago> For instance, Haskell is incapable of statically enforcing that file handles won't leak. It is capable of it. You can do it with one monad transformer layer per file handle. Yes, that's utterly inconvenient, but you can do it. It would be far less inconvenient with (higher) row types.
- catnaroek 10y agoThat forces you to deallocate resources in the inverse order of allocation, which may or may not be what you want. For example, the lock acquisition and release pattern used in the implementation of Lehman-Yao trees (and variants thereof) is inexpressible using monadic regions.
- tome 10y agoHmm, well I've never implemented this myself so I'm not going to contradict you, but I'd be really surprised if you can't hoist into the monad transformer stack and close off a lower region earlier than the top-level region.
- catnaroek 10y agoWould this be safe if the destructor of an inner region requires objects from outer regions to still exist?
- tome 10y agoI don't see how there could be any such requirements, if we're talking about file handles.
- catnaroek 10y agoI'm pretty sure DBMS implementors have to work with data structures whose nodes are spread across several files, and which have to be locked and unlocked in very precise ways to preserve the integrity of the data they contain. I'd like to be able these locking and unlocking patterns with types.
- evincarofautumn 10y agoI’m writing a functional language without a GC, so maybe I can offer some insight. The basic reasons you want a GC are closures, as you mention, and reference cycles. In a pure, eagerly evaluated language, cycles aren’t possible, so naïve reference-counting is enough to reclaim all garbage. If the language has mutable reference types, you can still use RC, but you need a cycle detector. Closures are more interesting. A neat thing about purely functional languages is that you can’t observe whether something is a value or a reference, so there’s no difference (apart from performance) between copying and sharing. Purely functional data structures take advantage of this to share common parts of modified structures. For instance, when replacing a node in a tree, you produce a new tree, but all nodes are shared except the O(log n) nodes in the path from the root to the new node. What I do in my language project, then, is to always copy (or move) values into a closure. If they’re unboxed, they’re probably small, and if they’re boxed, they’re in a reference-counted or copy-on-write structure, so sharing is safe. The next thing you can do is to unbox closures (like C++ lambdas). You only need to box them (like C++ std::function) if you want them to have uniform size, for example if you’re storing them in a homogeneous list. Finally, you tie value lifetimes to scopes. When a value goes out of scope, you drop it and reclaim its memory immediately (or place it on a queue, for deferred RC). That’s where stack-based languages become really handy, part of the reason I’m writing one. That’s also where substructural types become interesting, because they let you encode the answer to the question “When am I allowed to copy or drop this value?” Rust augments this with “lifetimes”, which are essentially a static approximation of stack depth. For instance, you can talk about references to objects on the stack, but you can’t store a reference to a younger value into an older value, because the younger one will be reclaimed first—its lifetime is not a supertype of the destination’s. This prevents the problem of dangling references. I don’t feel like substructural types are really geared toward functional languages, however; where they really shine is safe imperative programming like Rust—for a particular definition of “safe”, anyway.
- catnaroek 10y agoAgree with most of this, and even upvoted it, but... > I don’t feel like substructural types are really geared toward functional languages, however; where they really shine is safe imperative programming like Rust—for a particular definition of “safe”, anyway. I disagree with this. Even vanilla Standard ML and Haskell can benefit from substructural types. The structural rules (weakening, contraction) don't have to be an all-or-nothing proposition. Why should I be forced to pick one between garbage collection for in-memory data structures (which Rust won't give me, at least not out of the box), and deterministic reclamation guarantees for file handles and network connections (which ML and Haskell won't give me)? I want both!
- nickpsecurity 10y agoThe basic way to do it is just to allocate and delete things without the safety of a GC. ;) PreScheme did that among other things: https://en.wikipedia.org/wiki/PreScheme https://en.wikipedia.org/wiki/PreScheme The medium approach might be using linear types like in ATS language: http://www.ats-lang.org/ http://www.ats-lang.org/ http://ats-lang.sourceforge.net/PAPER/LDD-plpv07.pdf http://ats-lang.sourceforge.net/PAPER/LDD-plpv07.pdf The heavyweight approach is integrating one with a theorem prover with C extraction like in COGENT: https://ts.data61.csiro.au/projects/TS/cogent.pml https://ts.data61.csiro.au/projects/TS/cogent.pml https://ts.data61.csiro.au/publications/nictaabstracts/8956.pdf https://ts.data61.csiro.au/publications/nictaabstracts/8956.... Hope these give you ideas of how it can be done with what benefits and effort.