For example, with a procedural loop you can stick a debug statement in the middle to look at just about anything:
while (x=foo(y,z)) {
a = doStuff(x, z);
console.write("x={?},y={?},z={?},a={?}", x, y, z, a);
}For example, with a procedural loop you can stick a debug statement in the middle to look at just about anything:
while (x=foo(y,z)) {
a = doStuff(x, z);
console.write("x={?},y={?},z={?},a={?}", x, y, z, a);
}Also functional programming heavily encourages splitting up code into digestible chunks (they're called functions ;) )
buggyCode x y = if foo x
then bar x y
else baz y
I can wrap anything on the right-hand-side in a printf (except the keywords if/then/else). At the extreme end I could do: buggyCode x y = trace "hit buggy code" (if trace "applying foo to x" ((trace "hit foo" foo) (trace "hit foo's x" x)
then trace "applying bar x to y" ((trace "applying bar to x" ((trace "hit bar" bar) (trace "hit bar's x" x))) (trace "hit bar's y" y))
else trace "applying baz to y" ((trace "hit baz" baz) (trace "hit baz's y" y))
I could even move the x and y arguments across to the right-hand-side using anonymous function notation, then I can trace partial applications too: buggyCode = trace "hit buggyCode" (\x -> trace "gave x to buggyCode" (\y -> trace "gave y to buggyCode" <previous right-hand-side goes here>))
Note that we the built-in if/then/else is mostly a legacy crutch to aid familiarity. We can just use a function instead, and printf all the things: ifThenElse True x y = x
ifThenElse False x y = y
buggyCode = \x -> (\y -> (ifThenElse (foo x)
(bar x y)
(baz y)))
(Of course, defining our own if/then/else isn't much use; we're usually better off writing more meaningful, domain-specific alternatives)As for that `ifThenElse` example, there's nothing "meta" going on at all and there's no fancy control flow: it's just a (very simple) function. Consider a function like:
renderPage User page register = page
renderPage Guest page register = register
Doesn't look like 'clever spaghetti' to me: we branch between 'User' and 'Guest', returning a different one of our arguments for each. Yet this is identical to that 'ifThenElse' function, except for the names (which are irrelevant to the machine).This is what I meant by writing "more meaningful, domain-specific alternatives". Not to "re-invent control flow", but to avoid talking about control flow, and just use functions.
Note that we can do pretty much the same in any language, e.g. Python:
def ifThenElse(c, x, y):
return {True: x, False: y}[c]
def renderPage(session, page, register):
return {User: page, Guest: register}[session]
Nothing "meta" or "re-inventing the flow-control wheel" there; just some functions. Of course, this isn't a great idea in Python since Python is impure and uses eager evaluation.Note that such "meta" "reinvented control structures" appear all over the place in OOP, due to dynamic dispatch, e.g.
class User:
def renderPage(page, register):
return page
class Guest:
def renderPage(page, register):
return register
These are the two solutions to the "expression problem":- FP restricts the allowed values but allows new functions to use them. For example, adding an Admin session would be hard (need to rewrite renderPage), but adding a renderAvatar function is easy.
- OOP restricts the allowed functions (methods), but allows them to use new values. For example, adding an Admin class would be easy, but adding a renderAvatar method would be hard (need to rewrite User and Guest).
Scheme isn't, lambda is. That paper is about how lexical closures alone are enough to simulate a lot of imperative concepts like assignment, loops, and gotos (they do cheat and use set! in a couple places, but the point of the paper is how much lambda application can do all on its own). Your broader point about Lisps not being particularly functionally oriented is correct, though; they're some of the most unopinionated languages you could imagine.
On the other hand, that makes it much easier to debug code, by thinking along the lines of "p/2 is called after p/4" or even "p/2 returns A that is passed to p/4" (even though strictly speaking predicates don't "return" stuff).
For example, it's easy to forget how utterly baffled a learner can be when faced with:
x = x + 1Ps: I currently prefer my .NET as F# and so on but debuggability doesn’t feel biggest strength indeed altough it’s usually ok.
Also, Prolog code is often "split into digestible chunks". Even more than other languages, in fact, because it's rather hard to write nested loops or nested predicates or nested many other things.
If you were ever taught Prolog, it seems that you weren't taught well. You might want to risk a second look.
I find it very easy to write nested loops in Prolog:
findall(A
,findall(B
,findall(D
,findall(...)
,G)
,E)
,C)
To paraphrase the old saying, a good Python programmer can write Python in any language.Limerick: "Some rascal wrote Pascal in Haskell."
And a really good one can even write it in Prolog.
http://hps.ece.utexas.edu/pub/singhal_isca16.pdf
https://www.researchgate.net/publication/220760154_A_Prolog-...
Incidentally, my impression has been that the use of a declarative (or pure functional) language leads to spending drastically less (if any at all) time debugging. If your code makes sense (to you), it will work. Or, as an old adage goes, "If it compiles, it works."
First, many types of mistakes that are common when working with lower-level languages cannot occur at all when you use a declarative language. For example, with a logic programming language, you cannot accidentally double-free a pointer or write into unintended portions of memory.
Second, when working with a high-level language, the code is much shorter than if you had used a lower-level language. This makes it easier to debug, and also easier to get right already in the first few tries.
Third, once there is a mistake, it can be found in ways that fit the overall paradigm nicely. For example, in pure Prolog, if your predicate terminates and has a mistake, then it is either too specific or too general (or both), and you can debug both cases very systematically and even automatically, using built-in mechanisms of the language.
Declarative debugging methods for Prolog were pioneered by Ehud Shapiro in his 1982 PhD thesis, Algorithmic Program Debugging:
https://cpsc.yale.edu/sites/default/files/files/tr237.pdf
I also highly recommend Ulrich Neumerkel's teleteaching environment GUPU for systematically finding mistakes in Prolog programs:
Additionally, there are some constructs that are pretty damn hard to debug. For example- deeply-nested recursive loops with multiple clauses and free backtracking; you never know which clause you're in or how deep in the recursion. Well- you do but it demands a great deal of concentration.
Or take my personal tracing nightmare- DCGs. At some point, you inevitably end up with something like this:
_24234 = _12734
Then you have to squint up the trace to find the two variables to figure out what's going on.
Instead, focus on declarative properties: Does your predicate (or DCG) succeed in cases it shouldn't? Then your program is too general, and you need to add constraints. Or does your predicate fail in cases it should succeed? In that case, at least one of the goals is too specific. You can use declarative debugging to find out which goals are responsible for the failure.
You can add the following definition to your program to "generalize away" a goal by simply putting a * in front:
:- op(920,fy, *).
*_.
For example: pred :-
true,
false.
?- pred. now fails. Why? To find out, try to locate the reason for the failure by generalizing away goals. For example: pred :-
true,
* false.
This succeeds. So the last goal was responsible for the failure.GUPU finds such fragments automatically for you! In a very precise sense, they are explanations for the problem. I know of no other programming language that admits this approach.
That's good advice and I realise it articulates the way I reason about why my programs fail when they do (which is all the time).
The problem is when reasoning doesn't shed any light on what's wrong. There are many such situations for me, often caused by the fact that Prolog still manages to surprise me even after many years of programming in it.
For example I recently found out that findall/3 (which I use liberally) actually copies terms with fresh variables causing subtle changes to the structure of a term that are hard to detect with the naked eye. It took me I think an hour or so of debugging my code until I realised (and accepted) what was going on and I still had to go on to the Swi mailing list and ask, just to be sure. This is not something you can reason about, unless you know the semantics of a specific predicate (in this case, findall/3) in detail.
In another recent example I was debugging code that backtracked over the combinations of a set, waiting for that one combination that caused the problem to reveal itself. It's very difficult to just reason about situations like this, when almost everything works but there is a subtle bug hiding in the wings.
https://www.krin.gs/files/papers/krings_plspec_declare17.pdf
This isn't said about the declarative nature of languages so much as it is about the benefits of powerful static type systems, in my experience.
It's quite easy to write Prolog programs that look right (to me, someone who doesn't know Prolog well) but that compile, successfully run to completion, and produce incorrect results (or don't terminate). In Haskell, the compiler actually rejects a huge amount of incorrect programs that I thought were going to work (and the fact that I thought that shows that the declarative nature of the code wasn't sufficient) and then once the typechecker is finally happy, the code often does actually work without further debugging (this is a common experience, and what the saying is referring to, I believe); in Prolog (or dynamically typed functional languages like Scheme), there's very little of that, and I'm left to catch most bugs on my own (even if like you said there won't be as many because they'd be so obvious in the first place).
Prolog might be exceedingly clear once you learn how to read it and it might eliminate errors that way, but "if it compiles, it works" is a terrible description of that. "If it looks right to someone who knows the language well, it works" is much closer, but not as punchy. The entire point of the original saying is that you don't need the experience with the language to get that, because the compiler itself will tell you if you're wrong when your types won't line up.
As far as the nearby statement that maybe I didn't learn dissection correctly, that may indeed be the case, but was also my original point: perhaps the real problem is that nobody knows how to TEACH functional/logical programming right, and that's why they have not caught on in the mainstream despite being around several decades. I'd love to "get" the benefits, but it just won't click. Sometimes you have to teach programmers how to think, not just what to think.
SWI-Prolog's debugger can be invoked conditionally whenever you want from inside your running application. I've used it many times, and the code is simply:
(Condition -> gtrace ; true)
It's true that you could stick a write there, but you would have to think about what pieces of data you will need. Your mileage may vary. I've found this style a lot more useful than "traditional" debuggers where you set breakpoints beforehand and hope you hit them, as you say.> As far as the nearby statement that maybe I didn't learn dissection correctly, that may indeed be the case
The point was more specifically that you didn't learn Prolog properly (or at all) and don't realize that it forces you to dissect your code. As I said, in Prolog you cannot nest loops. You cannot nest loops. It's hard to write a big non-dissected piece of code if you cannot nest loops.
> perhaps the real problem is that nobody knows how to TEACH functional/logical programming right
I agree with regards to logic programming. We need better free tutorials. I don't particularly like the linked one, it feels like it doesn't start at a beginner's level. I would be interested if anyone who didn't know any Prolog before has managed to learn Prolog from it.
We got our introduction to it via the Tarkis' World book (90's edition).
Re: "in Prolog you cannot nest loops...It's hard to write a big non-dissected piece of code if you cannot nest loops." -- But nesting is good for modularity. It's sometimes called "step-wise refinement", which is a brilliant abstraction that is usually easy and natural to comprehend. Nesting is how we have been splitting up society since the dawn of civilization: countries, states, counties, cities, etc. (or equiv.) Similar for big militaries. (Maybe this is a meta form of Conway's law? Either way Conway was on to something.)
https://www.reddit.com/r/programming/comments/7pbkfv/great_f...
More recently, two university instructors started to use this book in courses that assume no previous knowledge about the language:
Andrej Bauer in his specialist elective course Principles of Programming Languages at the University of Ljubljana:
https://ucilnica.fri.uni-lj.si/course/view.php?id=67
Norbert Zeh in his Principles of Programming Languages lectures at Dalhousie University in Canada:
https://web.cs.dal.ca/~nzeh/Teaching/3136/index.html
Much of my current work is aimed towards making the teleteaching environment GUPU available in a free Prolog system:
https://www.complang.tuwien.ac.at/ulrich/gupu/
This will help a lot to teach Prolog to beginners in a scalable way.
On a rather personal, and maybe controversial, note: I regard the self-taught Prolog master as prevalent as for example the self-taught piano, chess or Zen master. In my experience, learning Prolog properly requires dedicated guidance, and it would be highly unusual to achieve mastery without it.
Where would you find such a master, though... If you are not studying at a university where Prolog is taught, you're pretty much out of luck, I think. (I am assuming that asking questions in newsgroups/IRC/etc is not a substitute for it.)
As in other disciplines, if you are elsewhere, you may have to move to get close to a master. This is not so unusual, is it? Vienna is a nice city for studying logic, and does attract students from all over the world!
> In my experience, learning Prolog properly requires dedicated guidance, and it would be highly unusual to achieve mastery without it.
There are two readings of this sentence. An unkind spirit might think you are saying that Prolog books cannot make you a master for some inherent reason related to Prolog, independent of any book. Whereas I would say that there is no inherent reason, it's just that currently existing Prolog books cannot make you a master because they are simply not very good. Or in other words, yes, guidance may be needed. But can it really not be guidance from a book?
In addition, for social and other reasons, you cannot write down in a book everything that advanced Prolog students should know. As a consequence, some things are better said than written, and I therefore think that a book alone is unlikely to be sufficient.
What do you mean here? My earlier comment was jokular, but you can definitely nest loops in Prolog:
...
deconstruct([L|Ls],Acc1,Bind):-
deconstruct(L,Acc1,Acc2)
,deconstruct(LS,Acc2,Bind).
...
etc. That's a nested loop, right there.Do you mean it in the sense that it's still the same predicate rather than a consecutive change of scopes?
foo_bar(Foo, Bar) :-
foo_bar_aux(Foo, Bar, 0).
foo_bar_aux(Foo, Bar, N) :-
...
Which in a language like OCaml would be hiding the loop in a recursive function defined inside the main one: let foo_bar foo =
let rec foo_bar_aux n =
...
in
foo_bar_aux 0
So yes, I think this is what you mean by being the same predicate rather than a "change of scopes".Of course there are many loop-like things that you can nest. If the arguments align just right, you can also use `maplist(maplist(maplist(baz)), ...)` instead of three auxiliary predicates, but I don't encounter many cases where this is possible or desirable.
You may want to try working with network before making this statement, when you cannot step through a function because of timeouts. Or maybe even looking at tracing debuggers, like strace, ltrace, or Erlang's gdb, which all do little more than "printing things to the console".
Logging (and logging-like tools) is a very important way of debugging services, you know.
I use logging to log problems that would happen on a user machine or server where I can't open the debugger and see it. When an error happens on a server or a customer machine and I can't use the debugger I have to guess the problematic section and put lots of logging and try to reproduce, then add more logging and so on until I find the problem.
Debugger are much better but I notice my other colleagues do not use them and prefer the console.log or similar
In my personal opinion and observation, the hierarchical step-wise refinement approach is quicker for most developers to grasp, learn, and debug. Maybe if those of us who don't see the grokkability benefits try it for 50 years, it will finally click. But you better hope to be in management by then because the programming field doesn't like "old people". I'm just the messenger.
It is like recursion, some problems can be solved nice with recursion but you do not use recursion everywhere. Also you can use a library in your favorite programming language and not use prolog, examples were presented in this thread.
My point is that a good programmer should know about the concept not to use it everywhere, I never had the chance to use prolog so far for my work.
Btw I am also past the stage where I have a favorite language and OS, every piece of software has it's issues.
Again I am just a noob, I learned Prolog and did some basic things years ago, a cool page I used to learn was this tutorial that implements a text based game , more refreshing then the tutorials with puzzles or the familiry
http://www.amzi.com/AdventureInProlog/a1start.php
The situation is maybe similar with GA(genetic algorithms) when you learn it you have some simple examples, when you want to solve a real problem things get complex fast(how to encode the problem,what fitness function should I use ...)
That's how you think and program in FP as well. Not just debugging, I mean: FP is all about divide and conquer. In fact, Wadler's seminal paper "Why Functional Programming Matters" [1] is all about what he calls modularity, i.e. splitting a problem in parts and then gluing it back together.
[1] https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.p...
Eh... What? It's procedural code that can change anything anywhere and thus can not be easily separated into independent pieces.
Bottom line- yes, you can add print statements etc, but you can also directly debug your Prolog code just like you'd do for an imperative language.