36

Is there a functional language which allows to use stack semantics - automatic deterministic destruction at the end of the scope?

user467799
  • 569
  • 1
  • 4
  • 6
  • Deterministic destruction is really only helpful with side-effects. In the context of *pure* functional programming, that just means ensuring that certain (monadic) actions always run at the end of a sequence. As an aside, it is easy to write a functional [concatenative language](http://en.wikipedia.org/wiki/Concatenative_programming_language) that doesn’t need garbage collection. – Jon Purdy Mar 10 '12 at 21:50
  • I'm interested in the contect of the question, whats one got to do with the otehr? – mattnz Mar 11 '12 at 01:29
  • 1
    In a functional language without garbage collection, I don't see how structural sharing of immutable data structures is possible. It may be possible to create such a language, but it's not one I'd use. – dan_waterworth Mar 12 '12 at 09:41
  • Rust has a lot of features commonly identified as 'functional' (at least, they're commonly wanted in non-func langs as functional features). I'm curious what it's missing. Immut by default, closures, func passing, principled overloading, ADTs (no GADTs yet), pattern matching, all without GC. What else? – Noein Aug 08 '15 at 00:42

3 Answers3

12

Not that I know of, though I'm no functional programming expert.

It seems pretty difficult in principle, because values returned from functions may contain references to other values that were created (on the stack) within the same function, or might just as easily have been passed in as a parameter, or referenced by something passed in as a parameter. In C, this issue is dealt with by allowing that dangling pointers (or more precisely, undefined behaviour) may occur if the programmer doesn't get things right. That's not the kind of solution that functional language designers approve of.

There are potential solutions, though. One idea is to make the lifetime of the value a part of the type of the value, along with references to it, and define type-based rules that prevent stack-allocated values from being returned from, or referenced by something returned from, a function. I've not worked through the implications, but I suspect it would be horrible.

For monadic code, there's another solution which is (actually or almost) monadic too, and could give a kind of automatically deterministically-destructed IORef. The principle is to define "nesting" actions. When combined (using an associative operator), these define a nesting control flow - I think "XML element", with the left-most of the values providing the outer begin-and-end-tag pair. These "XML tags" are just defining ordering of monadic actions at another level of abstraction.

At some point (at the right hand side of the chain of associative composition) you need some kind of terminator to end the nesting - something to fill the hole in the middle. The need for a terminator is what probably means the nesting composition operator isn't monadic, though again, I'm not entirely sure as I haven't worked through the details. As all applying the terminator does is convert a nesting action into effectively a composed normal monadic action, maybe not - it doesn't necessarily affect the nesting composition operator.

Many of these special actions would have a null "end-tag" step, and would equate the "begin-tag" step with some simple monadic action. But some would represent variable declarations. These would represent the constructor with the begin-tag, and the destructor with the end-tag. So you get something like...

act = terminate ((def-var "hello" ) >>>= \h ->
                 (def-var " world") >>>= \w ->
                 (use-val ((get h) ++ (get w)))
                )

Translating to a monadic composition with the following execution order, each tag (not element) becoming a normal monadic action...

<def-var val="hello">  --  construction
  <def-var val=" world>  --  construction
    <use-val ...>
      <terminator/>
    </use-val>  --  do nothing
  </def-val>  --  destruction
</def-val>  --  destruction

Rules like this could allow C++-style RAII to be implemented. The IORef-like references cannot escape their scope, for similar reasons to why normal IORefs can't escape the monad - the rules of the associative composition provide no way for the reference to escape.

EDIT - I nearly forgot to say - there's a definite area I'm unsure about here. It's important to ensure that an outer variable can't reference an inner one, basically, so there must be restrictions one what you can do with these IORef-like references. Again, I haven't worked through all the details.

Therefore, construction could e.g. open a file which destruction closes. Construction could open a socket which destruction closes. Basically, as in C++, the variables become resource managers. But unlike C++, there are no heap-allocated objects that cannot be automatically destructed.

Although this structure supports RAII, you still need a garbage collector. Although a nesting action can allocate and free memory, treating it as a resource, there's still all the references to (potentially shared) functional values within that chunk of memory and elsewhere. Given that the memory could be simply allocated on the stack, avoiding the need for a heap free, the real significance (if there is any) is for other kinds of resource management.

So what this achieves is to separate RAII-style resource management from memory management, at least in the case where RAII is based on simple nesting scope. You still need a garbage collector for memory management, but you get safe and timely automatic deterministic cleanup of other resources.

  • 2
    I fail to see why a GC is *necessary* in all functional languages. if you have a C++ style RAII framework in place, the compiler can use that mechanism as well. Shared values are no problem for RAII frameworks (see C++'s `shared_ptr<>`), you still keep deterministic destruction. The one thing that is tricky for RAII are cyclic references; RAII works cleanly if the ownership graph is a Directed Acyclic Graph. – MSalters Mar 12 '12 at 10:27
  • The thing is, functional programming style is practically built around closures/lambdas/anonymous functions. Without GC, you don't have the same freedom to use your closures, so your language becomes significantly less functional. – comingstorm Mar 12 '12 at 17:29
  • 1
    @comingstorm - C++ has lambdas (as of C++11) but no standard garbage collector. The lambdas carry their environment in a closure too - and elements in that environment may be passed by reference, as well as the possibility of pointers being passed by value. But as I wrote in my second paragraph, C++ allows the possibility of dangling pointers - it's the programmers (rather than the compilers or runtime environments) responsibility to ensure that never happens. –  Mar 12 '12 at 23:19
  • @MSalters - there are costs involved in ensuring that no reference cycles can ever be created. It's therefore at least non-trivial to make the language responsible for that restriction. Assigning to a pointer probably becomes a non-constant-time operation. Though it may still be the best option in some cases. Garbage collection avoids that issue, with different costs. Making the programmer responsible is another. There's no strong reason why dangling pointers should be OK in imperative but not functional languages, but I still don't recommend writing pointer-dangling-Haskell. –  Mar 12 '12 at 23:32
  • I would argue that manual memory management means you don't have quite the same freedom to use C++11 closures as you do Lisp or Haskell closures. (I am actually quite interested in understanding the details of this tradeoff, as I would like to write a functional systems programming language...) – comingstorm Mar 13 '12 at 00:26
  • @comingstorm - There's trade-offs both sides. The GC itself can limit your freedoms - GCs have costs, and many GCs accumulate those costs until they run a collection, paying off that debt (by Murphys Law) at the most inconvenient moment. That may limit your freedom to do things which add to that debt (e.g. heap-allocating more closure records by calling lambdas), and may prevent you using GC at all. –  Mar 13 '12 at 21:26
  • @comingstorm - I realised I wasn't sure about the lifetime of C++ lambdas myself, so I looked this up - http://stackoverflow.com/questions/7941562/what-is-the-lifetime-of-a-c-lambda-expression - it looks to me like stack-allocation of the object (and closure) plus the old C++ return value optimisation. So freedom is quite restricted with C++ lambdas - they're clearly focussed on making STL-style calls easier. This is, however, a useful subset of cases - and safe from reference-cycle issues. –  Mar 13 '12 at 21:47
2

If you consider C++ to be a functional language (it has lambdas), then it is an example of a language that doesn't use a garbage collection.

BЈовић
  • 13,981
  • 8
  • 61
  • 81
  • 9
    What if you don't consider C++ to be a functional language (IMHO it isn't, although you can write a functional program with it, you can also write some extremely non-fucntional (functioning.... ) programs with it) – mattnz Mar 10 '12 at 20:53
  • @mattnz Then I guess the answer doesn't apply. I am not sure what happens in other languages (like for example haskel) – BЈовић Mar 10 '12 at 20:57
  • 10
    Saying C++ is functional is like saying that Perl is Object-Oriented... – Dynamic Mar 10 '12 at 21:45
  • At least c++ compilers can check for side effects. (via const) – tp1 Mar 10 '12 at 23:09
  • 1
    @tp1 - (1) I hope this isn't going to regress into who's language is best, and (2) that's not really true. First, the really important effects are mostly I/O. Second, even for effects on mutable memory, const doesn't block them. Even if you assume there's no possibility to subvert the typesystem (generally reasonable in C++), there's the issue of logical constness and the "mutable" C++ keyword. Basically, you can still have mutations despite const. You are expected to ensure that the result is still "logically" the same, but not necessarily the same respresentation. –  Mar 11 '12 at 03:12
  • @tp1 - an effect that mutates the representation without mutating the logical value is still a significant effect in various ways - especially in a concurrent program, but also WRT reasoning about performance and other issues. So this support for logical constness is something you have in C++ but not in Haskell, but it's also something you pay for in C++ whether you use it or not, since the compiler often has to assume that objects are potentially only logically const because it can't prove whether some component, or referenced component, is or is not mutable due to separate compilation. –  Mar 11 '12 at 03:18
  • @tp1 - in truth, C++ has a pure functional subset which is Turing complete and quite useful - but only really useful for a specialised kind of task done at compile-time. I'm of course talking about templates. While there's certainly a pure functional subset of run-time C++, it's not practical to use just that subset for much. Usually, in C++, once you've written more than a single expression (and sometimes even within an expression) you have caused some kind of effect. –  Mar 11 '12 at 03:30
  • @tp1 - Of course some larger computations might have referential transparency anyway (rather like something you'd do with unsafePerformIO), but in general, it doesn't work out like that. Haskell provides the tools for dealing with immutable data structures etc, whereas C++ makes it quite hard to avoid mutation for anything substantial. –  Mar 11 '12 at 03:32
  • It's not a problem that it's possible to do side effects. The key trick is to make compiler check for problems in the area. If you make _all_ member functions of a class const member functions and then put all the data inside classes, then you have full functional programming in c++. – tp1 Mar 14 '12 at 19:37
2

I have to say that the question is a bit ill-defined because it assumes that there is a standard collection of "functional languages". Almost every programming language supports some amount of functional programming. And, almost every programming language supports some amount of imperative programming. Where does one draw the line to say which is a functional language and which is an imperative language, other than guided by cultural prejudices and popular dogma?

A better way to phrase the question would be, "is it possible to support functional programming in a stack allocated memory". The answer is, as already mentioned, very difficult. Functional programming style promotes the allocation of recursive data structures at will, which requires a heap memory (whether garbage collected or reference counted). However, there is a pretty sophisticated compiler analysis technique called region-based memory analysis using which the compiler can divide the heap into large blocks that can be allocated and deallocated automatically, in a way similar to stack allocation. The Wikipedia page lists various implementations of the technique, for both "functional" and "imperative" languages.

Uday Reddy
  • 297
  • 1
  • 9
  • 1
    IMO reference counting *is* garbage collection, and having a heap doesn't imply that those are the only options anyway. C mallocs and mfrees using a heap, but has no (standard) garbage collector and only reference-counts if you write the code to do that. C++ is almost the same - it has (as standard, in C++11) smart pointers with built in reference counting, but you can still do manual new and delete if you really need to. –  Mar 11 '12 at 23:25
  • A common reason for claiming that reference counting isn't garbage collection is that it fails to collect reference cycles. That certainly applies for simple implementations (probably including C++ smart pointers - I haven't checked) but it's not always the case. At least one Java virtual machine (by IBM, IIRC) used reference counting as the basis for its garbage collection. –  Mar 11 '12 at 23:27
  • Since all none tail recursive functions can be converted to tail recursive by "continuation passing", it should in fact be possible. Tail recursion can be handle solely on the stack, hence no GC. This often leads to slow performance but that was not the question. – kam Aug 21 '21 at 22:20