43

I came across an odd problem when writing an interpreter that (should) hooks to external programs/functions: Functions in 'C' and 'C++' can't hook variadic functions, e.g. I can't make a function that calls 'printf' with the exact same arguments that it got, and instead has to call an alternate version that take a variadic object. This is very problematic since I want to be able to make an object that hold an anonymous hook.

So, I thought that this was weird since Forth, JavaScript, and perhaps a plethora of other languages can do this very easily without having to resort to assembly language/machine code. Since other languages can do this so easily, does that mean that the class of problems that each programming language can solve actually varies by language, even though these languages are all Turing complete?

Mr. Minty Fresh
  • 592
  • 1
  • 5
  • 11
  • 33
    You should look into turing tarpits. They're a fascinating type of programming language that intentionally has as few operations available as possible while still being turing complete. Most of them lack basic data types, functions, even simple things like declaring variables. Personally, I think coding in them is great fun, since it forces you out of your comfort zone to try something needlessly difficult. – DJMcMayhem Sep 27 '16 at 20:08
  • Comments are not for extended discussion; this conversation has been moved to chat. – D.W. Sep 28 '16 at 17:17
  • Because I don't want a garbage collector taking up 5000% of my precious 492 allowed instructions. – Joshua Sep 29 '16 at 18:26
  • C++ absolutely can hook variadic functions. If your C++ can't do this it's hopelessly out of date. – Puppy Sep 29 '16 at 22:43
  • @Puppy Not with an arbitrary number of arguments and without recompiling. – user253751 Sep 30 '16 at 02:26
  • 8
    Look at what a Turing machine does, and you'll see that "Turing-complete" is a very low hurdle to clear. Basically anything that can 'read', 'write', and 'jump' while supporting basic arithmetic is Turing-complete. That's well below the level of the language features you're looking at. – aroth Sep 30 '16 at 03:17
  • 2
    To be even more ridiculous: the MOV instruction on an x86 processor is Turing-complete. See https://www.cl.cam.ac.uk/~sd601/papers/mov.pdf for the details. – Gareth McCaughan Sep 30 '16 at 11:01
  • 3
    Even the cardgame Magic: The Gathering is Turing-complete. Turing-completeness has almost nothing to do with the features of a language. – Mast Oct 01 '16 at 13:25
  • 2
    BTW there are also very complicated programming languages that aren't turing complete, such as those proof checkers. – 盛安安 Oct 01 '16 at 13:47

9 Answers9

71

Turing complete languages can compute the same set of functions $\mathbb{N}^k \rightarrow \mathbb{N}$, which is the set of general recursive partial functions. That's it.

This says nothing about the language features. A Turing Machine has very limited compositional features. The untyped $\lambda$-calculus is far more compositional, but lacks many features commonly found in modern languages.

Turing completeness tells nothing about having types, built in arrays/integers/dictionaries, input/output capabilities, network access, multithreading, dynamic allocation, ...

Just because Java does not have feature X (say, macros, higher-rank types, or dependent types), it does not suddenly stop being Turing complete.

Turing completeness and language expressiveness are two different notions.

chi
  • 14,564
  • 1
  • 30
  • 40
  • 44
    Edwin Brady, the designer of Idris, (only half) jokingly uses (I don't know whether he invented it) the term "Tetris-complete" to express this difference between "can compute any computable function on natural numbers" and "can be used to write non-trivial programs that interact with the environment". – Jörg W Mittag Sep 27 '16 at 21:31
  • 12
    You also might mention that you could have those things in C/C++. You'd just have to write some code that acts as a compiler in C/C++ for another language that has them where your code compiles a string in your C/C++ code. Then you can just be programming in something like Java within your C file. This would all be a lot of work, but it's possible (provably so because C/C++ is Turing Complete). – Shufflepants Sep 27 '16 at 21:47
  • 4
    @Shufflepants I wonder however if it is really useful to say "I can do it in C++ since I can interpret another language". By that token, Java, ML, C++ are equivalent. TM with an oracle for syscalls (I/O) are also equivalent. I fear that, by that reasoning, virtually all languages are equally expressive. If so, it is not a useful notion to compare languages. – chi Sep 27 '16 at 22:52
  • 1
    @Shufflepants, but your C compiler compiles to machine language, doesn't it? So really you can handle all those fancy abstractions in machine language. But wait...machine language is really just electrical and magnetic impulses when you get right down to it; there aren't even any 1s or 0s. So you're using electricity as your programming language! Wait a minute.... – Wildcard Sep 28 '16 at 07:36
  • 1
    The fact is that you as a living being with a mind are capable of assigning meaning, which is how you can make magical things happen with a lump of silicon and copper. But that goes a little beyond the scope of this site. – Wildcard Sep 28 '16 at 07:37
  • 1
    @Shufflepants I challenge you to rigorously prove or disprove that C is Turing-complete. I can just as easily argue to the contrary, and we'd get nowhere. The C standard does not provide a standard way to access unbounded memory so therefore it is not Turing-complete, but it also allows for undefined behaviour, so in theory, it allows for implementations that are far more powerful than Turing-complete models of computation (e.g. when the null pointer is dereferenced, the halting problem will be solved for an arbitrary program supplied through stdin). – Rhymoid Sep 28 '16 at 08:33
  • 2
    @Shufflepants Also: parsing C++ is undecidable because its type system is Turing-complete (which is very rare for programming languages), even though it might be argued that its dynamic semantics are not Turing-complete (in a way similar to C). – Rhymoid Sep 28 '16 at 08:35
  • 9
    @chi You are correct, they are equivalent. That's what it means to be Turing Complete. Anything that can be done with one Turing Complete system can be done in another Turing Complete system. It might not be convenient to do it in a particular system. And convenience of doing various things is the primary way we compare different programming languages. But that's not what the question was asking. – Shufflepants Sep 28 '16 at 14:56
  • 5
    @Rhymoid If C isn't Turing Complete for memory access reasons, or because being able to send arbitrary signals to a connected device which has arbitrarily large storage doesn't count, then one could argue that no real world language or device is Turing Complete. But I don't feel like that's a very productive line of reasoning. – Shufflepants Sep 28 '16 at 15:00
  • 2
    @Shufflepants Devices can't be Turing complete, just as circles can't physically exist. The distinction between bounded TMs and unbounded TMs is relevant in CS, because it draws the line between infeasible and impossible analysis. Not every CS concept has a direct application to software engineering. – Rhymoid Sep 28 '16 at 16:54
  • 1
    @Rymoid it's relevant in CS but not to this discussion. If we're truly only talking about abstract languages and not considering hardware or devices, then C IS properly Turing Complete because the size of an int isn't inherently defined by the language and therefor might be unbounded. That's the whole point of the "sizeof()" function to allow something to be run and compiled on a machine where a data type might be bigger. Any limitations are because of the particular compiler you've chosen or the machine you're running it on. – Shufflepants Sep 28 '16 at 17:58
  • 3
    @Shufflepants int has an unspecified bound, but has a bound. A language might be Turing equivalent because its semantics make it so, even if that semantics can not really be implemented because of physical limitations. Even in an abstract, ideal world, I am not sure whether C is actually Turing powerful, since <limits.h> states that every type is bounded, by a bound which is implementation dependent. So, if ideal-C can store unbounded information, it has to do it somewhere else (e.g. using file I/O, perhaps?). C is however at least "morally" Turing powerful, in my personal view. – chi Sep 28 '16 at 18:40
  • 1
    @chi When people talk about whether a given real world programming language is Turing Complete, they are virtually always talking about this "morally Turing powerful" you mention; that is "Turing Complete ignoring limits on memory that are inherently an artifact of the fact that the language was designed to actually be used in the real world where no such unbounded memory is physically possible". – Shufflepants Sep 28 '16 at 18:57
  • 1
    @Shufflepants Maybe. But e.g. the semantics of Java, Haskell, ML, Scheme, C# (I think), javascript, Perl, Python, PHP, etc is Turing complete without question (not just morally), since there's no thing as a pointer there whose size constraints the amount of memory we can address. In principle, I believe we could define a toy-JVM where object references are arbitrarily long, addressing an infinite memory space. Instead C is special in this respect by having in the language primitives which requires a bound on the memory. The mentioned languages AFAIK have not. – chi Sep 28 '16 at 22:51
  • 1
    If you want to be picky, C is turing complete, because there is no requirement that you can't keep calling "malloc" forever.

    You might say "but pointers are only X bits", but there is no requirement that C couldn't have a "hidden part" of a pointer you can't see or interact with, which could be of variable size (such an implementation would be weird, but legal). Also you could just make a pipe and use it as an infinite storage box

    – Chris Jefferson Sep 30 '16 at 09:05
  • 2
    @ChrisJefferson I agree on the pipe, but pipes are not ISO C. Files are, and might be used to achieve unbounded storage (not 100% sure on this). Pointers are not. If I memcpy a pointer, this copies exactly sizeof(void *) bytes, and the new pointer must have the same value. I don't think any ISO C implementation can have variable size pointers (hidden or not) and still achieve conformance. – chi Sep 30 '16 at 15:53
  • 1
    @chi And don't forget unbounded recursion, and the lack of bounds on the number of variadic functions. You could argue that C's semantics are possibly Turing-complete based on those two spec holes, and also that there is no conforming implementation of C. – Rhymoid Oct 01 '16 at 18:46
  • @Rhymoid The number of args in variadic functions is unlimited, but each call has to specify a specific number of args, and it looks trivial to bound the number of args used in actually performed calls. Unbounded recursion looks promising... but each arg must have an address in the bounded address space, so I'm not sure. Probably if we push it this much we indeed reach your conclusion: no conforming implementation can exist :-) The ISO standard IIRC has an escape clause to handle "hardware bounds" which applies here, so an implementation can be claimed conforming even with bounded recursion. – chi Oct 01 '16 at 20:14
  • @chi register values don't have to exist in an address space, so there's an infinite stack. Not sure if that helps though – k_g Mar 30 '17 at 02:19
49

Turing completeness is an abstract concept of computability. If a language is Turing complete, then it is capable of doing any computation that any other Turing complete language can do.

This does not, however, say how convenient it is to do so. Some features that are easy in some languages may be very difficult in others, due to design choices. Turing completeness just says that you can do the computation. As an extreme example, it may be difficult to hook varadic functions in C++, but it is possible to write a JavaScript interpreter in C++ which can hook variadic functions.

Language design is a pretty interesting art. One of the major steps one has to undertake is identifying which behaviors you want to form the backbone of your language. These behaviors are things that are easy to do in your language because they're built into the ground floor. We make design decisions about which features to include in every language.

As to your particular example, when C was developed, it was designed to operate very close to the way the assembly languages of the day operated. Variadic functions simply pushed arguments onto the stack, with very little typesafety. The implementation of these variadic functions was left to the compiler, to ensure maximum portability. Accordingly, very few assumptions were made about the capabilities of the hardware. By the time JavaScript came along, the scene had changed. It already operates in a virtual machine as an interpreted language, so the balance shifts towards convenience. Permitting hooking of variadic functions becomes reasonable. Even in the case of JavaScript that is Just In Time Compiled, our compilers are willing to store far more extra information about the arguments than the C compilers of yore were willing to store.

Cort Ammon
  • 3,351
  • 13
  • 16
  • The corollary here is that the machine language used by your CPU must be Turing Complete, or you wouldn't be able to run or compile "higher-level" Turing Complete languages on them in the first place. – Wildcard Sep 28 '16 at 07:39
  • 1
    @Wildcard I'd regard (virtually) all compilers as unsound w.r.t. this. Most languages are Turing-complete, but eventually need to be interpreted by/compiled to assembly, which is not. But this is a necessary physical limitation -- hardware is never Turing powerful. Still, computability offers many useful concepts which do "apply", in a practical sense, to real-world computers. – chi Sep 28 '16 at 09:03
  • @chi, do you mean in the trivial sense that hardware does not have infinite storage space, or some other sense? In what way is assembly language not Turing powerful? (Perhaps there is an article you could link that explains this, if the explanation is long?) – Wildcard Sep 28 '16 at 09:50
  • 3
    @Wildcard In that trivial sense. Assembly (and C) has fixed-size pointers, which can address only a bounded amount of memory. We could, theoretically speaking, define an assembly where pointers are unbounded naturals, but I wouldn't call that "assembly" anymore -- I'd call that URM or something like that. In practice we simply pretend the physical bounds are large enough to allow our programs to run, so even if a computer is only a finite state machine (implying that it can not e.g. perform addition), we think more about it as a Turing machine (so addition is doable). – chi Sep 28 '16 at 10:05
  • 1
    @chi: No need for unbounded pointers; your hypothetical machine could have a tape of memory banks it could advance through if you really wanted unbounded storage. –  Sep 28 '16 at 14:40
  • 4
    @chi That is not quite accurate. First off, virtually everyone considers C to be Turing complete because we typically assign that phrasing around the assumption that "you have enough memory." For the very small number of people who have to worry about the more strict wording where such assumptions are invalid, C does not specify the size of a pointer, and does not specify a bound on how much memory can be addressed. So even in the absolute strictest sense of "Turing complete," C is indeed Turing complete. – Cort Ammon Sep 28 '16 at 15:01
  • 3
    @CortAmmon I have to disagree. If we formalized the semantics of C, trying to embed the "enough memory" assumption, we would fail since sizeof(void *) is mandated to evaluate to something by the ISO C standard. This forces us to bound the amount of memory for any given program, to something large -- but still a bound. E.g. I can not write a program whose semantics is adding two arbitrary naturals. C might still be made Turing powerful through I/O, using files like TM tapes (as @Hurkyl point out above). I do agree on this being a non-issue in practice. – chi Sep 28 '16 at 15:24
  • @chi: You might be able to finagle adding two arbitrary naturals by having the runtime pick the sizes of the fundamental types at launch time. However, that line of thought goes dead since in general, you cannot compute the memory requirements of a program. –  Sep 28 '16 at 16:05
  • 1
    @chi sizeof(void*) must evaluate to something, but the specification does not say what it must be, other than to say that it must be an integer. It may be an arbitrarily large finite number, just as the non-blank part of the turing tape may be arbitrarially large. If you use Hopcroft and Ullman's formal definition of a Turing machine, the number of non-blank cells on the tape must be finite, so sizeof(void*) can return a finite integer while still representing the entire tape in memory. – Cort Ammon Sep 28 '16 at 17:53
  • @chi "enough memory" doesn't mean "enough random access core memory addressable by a pointer that can be stored in a variable of type void *" – OrangeDog Sep 28 '16 at 17:57
  • @chi As an alternative solution, you can leverage the fact that ISO/IECN3242 section 1.7.1 defines the fundamental unit of storage in the C++ memory model to be a "byte." It explicitly does not specify how many bits are in a byte, other than that they are enough to contain any character in the basic execution character set and the eight bit codes in UTF-8. It provides no bound on the number of bits in a byte, so it would be completely valid to have a byte such that the cardinality of the set of bits in that byte is infinite, giving it more than enough range to address any spot on the tape. – Cort Ammon Sep 28 '16 at 18:03
  • @chi it's very easy to write a C program that adds two arbitrary naturals if the the numbers are expressed as unary strings separated by a space on stdin, and the output is likewise a unary string on stdout. – OrangeDog Sep 28 '16 at 18:03
  • 1
    @CortAmmon The <limits.h> macro CHAR_BIT states how many bits in a byte, making it finite. I personally classify C as "morally Turing equivalent", since if it is not (I am not 100% sure either way), it's only because of little details, and not because it lacks general computation features. – chi Sep 28 '16 at 18:29
  • 1
    @OrangeDog If the addressable memory is bounded, how to store unbounded information? And yes, in unary we can perform addition, but so can a FSA with I/O. We still can not decide $0^n1^n$. – chi Sep 28 '16 at 18:32
  • @chi C is definitely "morally Turing equivalent," but I think if you really push it, we find that it is actually Turing equivalent. The CHAR_BIT issue isn't really an issue because a finite number of bits is sufficient to encode an infinite number of values, as the number of values encodable is the power-set of the set of bits. (and for full disclosure, the way I see it, I am either right, or I'm going to get to learn something that I didn't know about set theory to show that I'm wrong, so I'm having fun exploring this technicality) – Cort Ammon Sep 28 '16 at 18:41
  • 7
    I suggest the new language C-inf, which is exactly like C, except when too much memory is allocated through recursion or heap allocation, the program is aborted, recompiled with a larger value for sizeof (void*) and sizeof (size_t), and starts running again from the start. – gnasher729 Sep 28 '16 at 21:16
  • 1
    @gnasher729 That should work. We could also keep sizeof(void*) fixed and increase instead sizeof(int) on overflow, effectively making int arbitrary-precision. In such way, we no longer need malloc/recursion. This "retry-on-out-of-memory" mechanism diverges from the ISO semantics, of course, but is the best way I've seen so far to make C Turing-equivalent while not diverging too much. In practice, if you wanted to implement that you need to "undo" the side-effects like I/O when you retry, so it is unrealistic, but at least in theory it works fine, AFAICS. – chi Sep 29 '16 at 12:39
  • BTW, assembly language hasn't changed much: we still use register machines with a call-stack that's used for automatic storage, and static and dynamic storage still work the same as in early Unix. Modern ABIs (calling conventions) pass the first few args in registers instead of the stack, but that doesn't matter. C is still very close to asm. One C operator usually compiles to at most a few asm instructions, and there aren't any string-concat operators or things like that which compile into loops that you didn't ask for. (Auto-vectorization by optimizing compilers can create bloated code.) – Peter Cordes Sep 30 '16 at 18:50
26

Think of programming languages as different land vehicles: bicycles, cars, hovercars, trains.

Turing Completeness says "this vehicle can go anywhere any other vehicle can go." That is, you can compute all the same functions. Input to output, start to end.

But, that statement says nothing about how you get there. It might be on rails, it might be on roads, it might be in the air. In the same way, Turing Completeness says nothing about how you compute a function. You might use recursion, or iteration, or some weird cellular automata. You might use types or not, you might use dynamic or static techniques. But, if all you consider is functions (or sets/formal languages) you can compute, as long as you are Turing Complete, these features give you the same power.

Joey Eremondi
  • 29,754
  • 5
  • 64
  • 121
  • 4
    This is a great analogy. It also extends nicely to the question I've seen elsewhere on this site, of whether or not there could be other models of computation that surpass a Turing machine: In this analogy, airplanes and spaceships are more than Turing Complete, and speedboats are another type of machine entirely. :) – Wildcard Sep 28 '16 at 07:42
  • 2
    Faster than Light travel is probably a better analogy for super-Turing computation. It may be possible, but most people think it's not. – Joey Eremondi Sep 28 '16 at 23:19
  • @jmite Of course, there's still no good evidence to suggest our brains are super-turing computers. Our (apparent) inability to consider non-turing machines might stem from that, though it's not necessarily an insurmountable barrier. Airplanes are actually quite a good analogy - they just go "straight" between two points, ignoring the terrain. If we could ignore the topology of spacetime itself, we could fly faster than light as well. Not that I'm saying it's possible to ignore the topology of spacetime, mind you :) – Luaan Sep 30 '16 at 14:26
  • 1
    @Luaan Right, but our brains don't necessarily need to be super-Turing to comprehend a super-Turing computer. I can describe the semantics of a Turing Machine using a weaker terminating language, like Simply Typed Lambda Calculus, by writing a function that takes a TM and its state, and steps it to the next state. I can't actually run the machine in that language (because it might take infinite steps), but I can write what each step looks like. – Joey Eremondi Sep 30 '16 at 15:46
  • @Luaan, "there's still no good evidence to suggest our brains are super-turing computers" —perhaps, but there is also no evidence to suggest that the human mind is only a Turing machine. Since there is no Turing machine that can be pointed to anywhere that can't be traced to ideas originated by a human mind—there is still the distinction that life can originate ideas, and mechanical contrivances cannot. But as for models of computation, I think Turing machines do successfully encompass anything that could be reasonably called "computation," ideas and dreams and such notwithstanding. – Wildcard Dec 05 '18 at 23:02
17

What you are essentially asking about is the difference between the computational power and what is commonly called the expressive power (or just expressiveness) of a language (or system of computation).

Computational Power

The computational power refers to what kinds of problems the language can compute. The most well-known class of computational power is that which is equivalent to a Universal Turing Machine. There are a lot of other systems of computation, such as Random Access Machines, λ-calculus, SK combinator calculus, µ-recursive functions, WHILE programs, and many others. And as it turns out, all of these can simulate each other, which means that they all have the same computational power.

This gives rise to the Church-Turing Thesis (named after Alonzo Church who created the λ-calculus and Alan Turing who created the Universal Turing Machine). The Church-Turing-Thesis is a hypothesis on computability with two aspects:

  1. all computing systems capable of general computation are equally powerful, and
  2. a human being following an algorithm can compute exactly the functions that a Turing Machine (and thus any of the other systems) can compute.

The second is more important in the field of philosophy of mind than computer science, though.

However, there are two things the Church-Turing-Thesis doesn't say, that are very relevant to your question:

  1. how efficient the various simulations are and
  2. how convenient the encoding of a problem is.

A simple example for (1): on a Random Access Machine, copying an array takes time proportional to the length of the array. On a Turing Machine, however, it takes time proportional to the square of the length of the array, because the Turing Machine does not have random memory access, it can only move across the tape one cell at a time. Therefore, it needs to move across the n elements of the array n times to copy them. So, different models of computation may have different performance characteristics, even in the asymptotic case, where we try to abstract away from implementation details.

Examples for (2) abound: both λ-calculus and Python are Turing-complete. But would you rather write a program in Python or in λ-calculus?

There is also a third wrinkle I have skirted around until now: all of those original systems were designed by logicians, philosophers, or mathematicians, not by computer scientists … simply because computers and thus computer science didn't exist. These all go back to the early 1930s, even before Konrad Zuse's very first experiments (which weren't programmable and/or Turing-complete anyway). They only talk about "computable functions on the natural numbers".

Now, as it turns out, there's a lot you can express as functions on natural numbers – after all, our modern computers even get by with a lot less than that (basically 3-4 functions on the numbers 0 and 1, and that's it), but, for example, what function does an operating system compute?

This notion of I/O, side-effects, interacting with the environment, is not captured by the idea of "functions over natural numbers". And yet, it is kind of important, since, as Simon Peyton Jones once put it "All a pure function with no side-effects does, is make your CPU hot", to which an audience member replied "actually, that is a side-effect, too!"

Edwin Brady, the designer of Idris, (only half) jokingly uses (I don't know whether he invented it) the term "Tetris-complete" to express this difference between "can compute any computable function on natural numbers" and "can be used to write non-trivial programs that interact with the environment". Even more ironically, he demonstrates this by having an implementation of a Space Invaders clone in Idris, but he says that he is confident that Tetris reduces to Space Invaders.

Another thing to point out is that not only is Turing-equivalence not necessarily enough to talk about actually writing "useful" programs, it may OTOH also not even be necesssary. E.g. SQL has only become Turing-equivalent with ANSI SQL:1999, but it was still useful before that. In fact, some might argue that making it Turing-equivalent hasn't added to its usefulness at all. There are many Domain-Specific Languages that aren't Turing-equivalent. Data Description Language typically aren't (and shouldn't be). Total Languages obviously can't be Turing-equivalent, yet you can still write event loops, web servers, or operating systems in them. There are also languages which are Turing-equivalent but where this is actually considered to be a mistake.

So, all in all, Turing-equivalence is not terribly interesting, unless you want to statically analyze programs.

Expressiveness

Assuming that our computation system is computationally powerful enough to even solve our problem at all, what we have to do next, is to express our algorithm for solving that problem in some sort of formal notation for that system. In other words: we need to write a program in some computer language. That's where the notion of expressiveness comes in.

It refers to, essentially, how "easy" or "enjoyable" it is to write our program in our particular programming language. As you can see, the notion is quite vague, subjective, and more psychological than technical.

However, there are attempts at more precise definitions. The most famous one (and the most rigorous one I know of) is by Matthias Felleisen in his paper On the Expressive Power of Programming Languages (the first two pages contain a gentle introduction, the rest of the paper is more meaty).

The main intuition is this: when translating a program from language to another language, some of the changes you need to make are locally contained (such as e.g. turning FOR loops into WHILE loops or loops into conditional GOTOs), and some require a change to the global structure of the program.

When you can replace one feature of one language with a different feature of a different language by only local transformations, then these features are said to have no effect on the expressive power. This is called syntactic sugar.

On the other hand, if it requires a change of the global structure of the program, then the language you are translating to is said to be unable to express the feature. And the language you are translating from is said to be more expressive (with respect to this feature).

Note that this gives an objectively measurable definition of expressiveness. Note also that the notion is context-dependent on the feature, and it is comparative. So, if every program in language A can be translated to language B with only local changes, and there is at least one program in language B which can not be translated to A with only local changes, then language B is strictly more expressive than language A. However, the more likely scenario is that many programs in both languages can be translated back and forth, but there are some programs in both languages that cannot be translated to the other. This means that neither language is strictly more expressive than the other, they just have different features that allow different programs to be expressed in different ways.

This gives a formal definition of what it means to be "more expressive", but it still doesn't capture the psychological notions behind the phenomenon. For example, syntactic sugar, according to this model, does not increase the expressive power of a language, because it can be translated using only local changes. However, we know from experience that having FOR, WHILE, and IF available, even if they are just syntactic sugar for conditional GOTO makes expressing our intent easier.

The fact is, different languages have different features that make expressing different ways of thinking about a problem easier or harder. And some people might find one way of expressing their intent easier and others a different way.

An example I found in the Ruby tag on StackOverflow: many users who follow the Ruby tag claim that loops are easier to understand than recursion and recursion is only for advanced functional programmers and loops are more intuitive for newcomers, but I have seen multiple cases of complete newcomers who intuitively write code like this:

def rock_paper_scissors
  get_user_input
  determine_outcome
  print_winner
  rock_paper_scissors # start from the top
end

Which usually leads to several people commenting that "this doesn't work" and "they are doing it wrong" and the "correct way" is this:

def rock_paper_scissors
  loop do
    get_user_input
    determine_outcome
    print_winner
  end
end

So, clearly, there are some people for whom tail recursion is a more natural way to express the concept of "looping" than loop constructs.

Summary

The fact that two languages are Turing-equivalent says one and exactly one thing: that they can compute the same set of functions on natural numbers as a Turing Machine can. That's it.

It doesn't say anything about how fast they compute those functions. It doesn't say anything about the ease of expressing those functions. And it doesn't say anything about what else they can do besides computing functions on natural numbers (e.g. linking to C libraries, reading input from the user, write output to the screen).

does that mean that the class of problems that each programming language can solve acctually varies by language, even though these languages are all turing complete?

Yes.

  1. There are problems that aren't covered by the term "Turing-complete" (which only concerns itself with computing functions on natural numbers) such as printing to the screen. Two languages can be Turing-complete but one can allow printing to the screen and the other not.
  2. Even if both languages can solve the same problems, that doesn't say anything about how complex the encoding is, and how easy it is to express this encoding. E.g. C can solve every problem Haskell can, simply by writing a Haskell interpreter in C … but you have to write the Haskell interpreter first in order to solve a problem this way!
Jörg W Mittag
  • 6,140
  • 23
  • 25
7

All Turing complete programming languages can implement the same set of algorithms. So, if you see that some algorithm is very difficult to be implemented in a particular language, it does not mean it is impossible.

Remember that a language consists of syntax and semantics. Sometimes, the set of words belonging to some language is not minimum to be considered Turing complete, there are features that make things easier (that’s why they are called features). If you take out those features, the language is still Turing complete.

Some of these might be of interest:

Palec
  • 109
  • 1
  • 6
Euge
  • 630
  • 1
  • 4
  • 8
5

All Turing-complete languages can compute the same things.

If you try to implement a modern language, you'll notice that most of its features do not add any computational capabilities. Many of those features can be lowered to simpler ones that already exist in the same language.

Here are some examples:

  • If you don't have enums, you can use integers.
  • If you don't have buffers that know their size, you can create a type that contains a size and a buffer that doesn't know its size.
  • If you don't have bounds checking of buffers, you can just check the index every time you use them.
  • If you don't have variadic functions, you can create a function that takes a buffer that knows its size and read from that buffer the same information that you'd get from formal arguments.
  • If you don't have operators, you can use functions.
  • If you don't have types that can inherit each other, you can create types that contain each other and access them through an extra level of indirection, with subtyping being simply a transformation of the handle-to-the-entire-type to a handle-to-the-contained-type.
  • If you don't have delegates but you have function pointers, you can create a type that contains the referent object and a function pointer.
  • If you don't have delegates but you have interfaces, you can declare an interface that contains a single method with the signature you want.
  • If you don't have generic types, you can use non-generic types that only assume the upper or lower bounds you are interested in (and maybe perform appropriate casts at the usage spots, to keep the compiler happy).
  • If you don't have a linear/affine type system, you can just avoid using any variable more than once.
  • ...and so on.

Mainstream language design focuses on features that make it easier and more convenient for us to compute things faster, recognize our mistakes earlier, program against unknown components, make parallelism safer, and so on.

The purely computational stuff were nailed down a long time ago.

4

The existing answers rightly point out that Turing completeness is not a good way to compare languages. Indeed, almost all languages are Turing complete. ("If everyone is special, then no one is special," as The Incredibles used to say.)

However, it is possible to compare the expressiveness of languages with mathematical precision. Take a look Felleisen's On the Expressive Power of Programming Languages. Roughly, the idea is to ask the following question: Can I convert any program in language A into a program in language B by making only local changes? In other words, Felleisen gives a mathematically precise form to your intuition.

rgrig
  • 1,336
  • 1
  • 11
  • 15
3

On top of everyone else's answers, here's another analogy.

To wash clothes, you need three things: some reservoir which holds water, a detergent of some kind, and an agitation mechanism. This could be realised in many ways. The water reservoir is anything which holds enough water (e.g. a tub, a lake, a river). The agitation mechanism could be a mechanical device, a washboard, or even a rock against which the clothes are beaten. And detergents come in various forms, too.

So what's the difference between a modern computerised washing machine, and a rock next to a river?

It boils down to three things: efficiency, safety, and convenience. Some methods of washing use less energy, pollute the environment less, use less water, and so on. Some methods of washing require less repetitive manual activities (which result in injuries) or being outside in inclement weather. And some methods of washing don't require a human to babysit the process.

Turing-complete programming languages are general purpose, so they are put to more than one task. Nonetheless, for a given task, some programming languages are more efficient, more convenient, and safer (in the sense that less can go wrong when the program is actually used) than others.

Pseudonym
  • 22,091
  • 2
  • 42
  • 84
2

Others have provided plenty of good answers, but they don't explicitly mention one caveat that once confused me a lot: Turing completeness doesn't imply that a language can express arbitrary computable functions from its inputs to its outputs. It is weaker: there must be some way of representing the domain and range of the set of computable functions as inputs and outputs such that each of these functions maps to a program that takes a representation of its inputs to the corresponding outputs.

Take, for instance, a language that expresses Turing machines. Each program in the language is a Turing machine.

Now consider the sublanguage of all Turing machines that read and write only the characters a, b, and blank. It is Turing complete, but it cannot express any programs that, for instance, produce c on all inputs, because it can't write any cs. It can only express all computable functions on inputs and outputs encoded as strings of as and bs.

So it isn't true that all Turing-complete languages can compute the same things, not even when we limit these things to be the computable functions from their potential inputs to their potential outputs. The language may require inputs and outputs to be encoded in certain ways.

reinierpost
  • 5,509
  • 1
  • 21
  • 38