tag:blog.c3lang.org,2013:/posts c3designer's blog 2015-06-22T20:33:35Z tag:blog.c3lang.org,2013:Post/872532 2015-06-22T20:33:35Z 2015-06-22T20:33:35Z Rationale for “Aims and ‘Rules of Thumb’ for the Design of C³”

Bjarne Stroustrup presents the aims and his design “rules of thumb” for C++ in “The Design and Evolution of C++” (D&E) and in “Evolving a language in and for the real world: C++ 1991-2006”. He explains: “To be genuinely useful and pleasant to work with, a programming language must be designed according to an overall view that guides the design of its individual language features. For C++, this overall view takes the form of a set of rules and constraints.”

For C³, I want to revisit and refine those aims and rules. The result will be my own design rules that will help me come up with the definition of C³ (hopefully with some collaborators). C++ is the main inspiration for C³ and studying its goals is a great way to find my own priorities. I will discuss each C++ rule and give the new C³ rules as part of the text, preceded by an arrow. There is a summary of the new list at the end.

I hope this will give you a small taste of what C³ will eventually become but keep in mind that this is not a description of C³! Even if you would like to see more practical examples, this is sometimes impossible since the details are not designed yet. Stroustrup did not publish his rules until his language was already widespread. Please bear with me as I don't have the benefits of an hindsight perspective.

The text is written in present tense even if the features does not exist yet. This will help me keep this document up to date when they appear for real! I don't explain in details the original rules for C++ so reading (or re-reading) the chapter 4 of D&E wouldn't hurt.

Aims

C++ makes programming more enjoyable for serious programmers.

This is clearly in line with my own view of what programming is all about. We have problems to solve and it's a joy to make the computer do the work for us. It's even more enjoyable when the programming language makes that task easy or makes our programming work more useful for the world.

Some language designers take away some freedom in order to make their language easy to use. This is not acceptable for the serious programmer who often has changing needs. If the language is too inflexible to solve some specific problems, it fails at this aim because it is usually not very enjoyable to mix and match different programming languages. Within the language, the easy tasks must be easy to do and the hard ones must be possible.

⇒ C³ makes programming more enjoyable for serious programmers.

C++ is a general-purpose programming language that

  • is a better C 
  • supports data abstraction 
  • supports object-oriented programming 
  • supports generic programming

First, this long “aim” defines C++ as a “general-purpose programming language”. Since C³ is designed to be a successor for C++, I keep this part unchanged. In fact, it supports an even greater range of purposes, covering territories traditionally occupied by higher-level (scripting) or lower-level (assembly) languages.

 C³ is a general-purpose programming language.

The bullet points define the programming styles that are supported by C++. This is very important because the features of the language are there to allow these different ways of programming. This is a long discussion so I split it by “paradigm”:

C++ is a better C

C³ differs from C++ because it does not aim for source-level C compatibility. It is a known deficiency that I'm willing to introduce. It enables me to make C³ a more enjoyable and powerful language. It allows a lot of improvements, from the smallest annoyance (like the weird declarator syntax and the inconsistent conversion rules) to the fundamental concepts (what is a variable?).

There is a lot of new software projects being started every day, enough to make C³ a viable choice for many people, even without C compatibility. C³ is not retrocompatible with any other language either. This is somewhat counterbalanced by the very general semantics. They enable programmers to build transition paths from any language in an almost seamless way. For example, the C³ type system allows the programmer to define types that mimic the behavior of Java object references.

I don't keep "be a better C" as a aim, but I build on what made C a success! I keep the syntax familiar to practitioners of any C-family language. I also list two concepts that C³ borrows from C, with some improvements:

The programming style supported by C is called imperative programming. It's important because it's the way most people program and, moreover, it's the way actual computers work! This “follow the recipe” style is often the simplest way to describe algorithms. For the sake of brevity, what I call imperative programming also includes procedural, structured and modular programming. All are supported by C but there is some room for improvement here and there.

⇒ C³ supports imperative programming.

I call low-level programming the facilities for mapping the high-level syntactic constructs of C³ to the native environment. It does not imply that the target environment is primitive. It just means that the programmers has access to all features directly available in the environment.

Low-level programming in C³ means embracing the law of leaky abstractions. That is, when an abstraction shows its weakness, all the underlying layers are available for inspection and extension.

⇒ C³ supports low-level programming.

C++ supports data abstraction

Data abstraction is the ability to create new data types. It is what allows us to create value types, for example complex numbers and strings. The main idea here is the encapsulation of a private representation behind a simple public interface. This interface and its associated semantics constitute what's classically called an Abstract Data Type (ADT). Algorithms that use ADTs are much simpler than their equivalent that manually handle value representations. 

Doug Lea explains what ADT programming is in "C++ as a Data Abstraction Language":

"Pure ADT programming is value-oriented rather than object-oriented. The basic rules of value-oriented programming are that you are never allowed to care about (or in most ADT languages, even to discover) which object is being used to represent something; you only care about the symbolic meaning of the value itself. Named variables are considered to be bound to different values; and are not considered to be handles to objects. Because programs deal with computers, not mathematical abstractions, there must indeed be some object that maintains the representation of any given value used in a program. But again, in ADT style programming you are not supposed to know anything about this."

He then goes on to explain what rules and techniques must be used to program in this style in C++.  It quickly becomes quite complicated when it comes to argument passing strategies and the handling of dynamically allocated representation. With its focus on object-oriented programming, and despite the fact that this was an early aim, ADT support has never been that good in C++ except for very small objects like complex numbers. Even fundamental types like strings were expected to use copy-on-write semantics that are now considered obsolete in the presence of manycore programming. (See “Optimizations That Aren't (In a Multithreaded World)” by Herb Sutter.)

A new programming paradigm is now gaining popularity. It's called "generic programming". I will talk about it in its own section below but I introduce it here because it's based on ADTs. C++ templates allowed Alex Stepanov to do generic programming and he created the STL. As a result, ADTs are much more used than before and the shortcomings of C++ in this area are becoming a real problem. C++0x contains new features aimed at solving the most obvious problems but at the price of some quite awful complexity.

C³ solves these problems in two ways. First, it decouples the values from the objects used to hold them. In particular, a variable is semantically bound to different values but it's up to the compiler to decide what object is used to hold them. This allows more efficient argument passing and automatic resource sharing. Second, C³ does not conflate value types and object-oriented classes. This make sure that when you want an ADT, you don't accidentally introduce dependencies on the object's identity.

This is a paradigm where C³ differs a lot from C++ but it embraces and extends the innovative features that allowed data abstraction in C++. The most important ones are constructors and destructors. The biggest difference is the assumption that value types will be used for most data instead of object-oriented classes, even for big and complicated data.

⇒ C³ supports data abstraction.

C++ supports object-oriented programming

C³ acknowledges object-oriented programming as a useful style in some circumstances but there is no need to evangelize it as some C++ proponents did in the 1980s. It is certainly not a requirement to use objects (involving message-passing semantics) in every software project as proponents of Java and C# do. C³ offers direct syntactic support for message passing that mimics C++ in most cases and its library contains an implementation of C++-like semantics. It could also support more dynamic semantics, for example duck typing and multi-methods, with appropriate library implementations.

The main problem of OOP is that a lot of people are confused between data abstraction features and object-oriented ones. The fact that types and classes are two different things in C³ will probably help people to learn the difference! (See “On Understanding Data Abstraction, Revisited” by William R. Cook.)

⇒ C³ supports object-oriented programming.

C++ supports generic programming

Generic programming is a programming technique that aims to make programs more adaptable by making them more general. The highlight of generic programming is the ability to create highly reusable components with absolutely no performance penalty.

Generic programming was not part of the initial aims of C++ and it shows. Templates were initially created in order to implement type-safe containers and their supporting operations, not generic algorithms. The syntax is not as terse as for object-oriented style because Bjarne Stroustrup did not anticipate generic programming as a fundamental style. Fortunately, templates were expressive enough to allow Alex Stepanov to implement STL but he also said “I think that it is possible to design a language based on C and on many of the insights that C++ brought into the world, a language which is more suitable to this style of programming.” C³ aims to be that language.

⇒ C³ supports generic programming.

C³ also supports programming styles that were not part of the C++ rationale: functional programming and metaprogramming.

Functional Programming

Being a functional programming language might seem in conflict with being an imperative language but it's not, for very fundamental reasons. I'll just give the beginning of an explanation by saying that the C³ compiler internally transforms all code to static single assignment form (SSA form). This is usually done  to perform optimizations but in C³, this is also very important for the semantics of user-defined types and functions. It has been proved that SSA is functional programming. It means that translation between the two paradigms is possible!

In C³, imperative programming is built on top of functional programming. When doing imperative programming, everything that looks like a side effect is translated to a purely functional form. Even low-level components are specified in a functional way. We could say that imperative programming is only syntactic sugar for functional programming but that would be dismissing the complexity of the translation.

Functional programming usually come with more features than just a lack of side effects. The support in C³ includes first-class functions, currying, and guaranteed tail-call optimization. Evaluation is strict but I hope that the user-defined type facilities will be general enough to implement the semantics of lazy evaluation in a library type if needed.

⇒ C³ supports functional programming.

Metaprogramming

Metaprogramming is the writing of programs that manipulate programs as their data. It is sometimes very useful to be able to write our programs as a transformation of existing programs. In these cases, writing the final program by hand would be more work and less maintainable.

A compiler is a metaprogram: it translates a program from a source language (for example, C³) to a target language (for example, machine code). Another example is a parser generator, like YACC, that generates a syntactic analyser from a context-free grammar. These examples are external tools written from scratch with no special language support.

When we say that a programming language supports metaprogramming, it usually means support in the language itself, with no external tool. That is, the language supplies a complete interface to manipulate programs with ordinary code. This is better than building external tools because the manipulation is done on the abstract syntax tree of programs and the programmer does not need to work directly with text.

C++ allows metaprogramming but only as an ugly byproduct of the template mechanism. We can’t use the other programming styles like imperative programming to write metaprograms in C++. In C³, the difference between “normal” and “meta” code is kept to a minimum.

A benefit of metaprogramming that’s worth mentioning is the fact that it allows programmers to experiment with new programming paradigms, even if the language does not explicitly support them.

⇒ C³ supports metaprogramming.

General rules

C++’s evolution must be driven by real problems.

Well... this first rule is a great way to make sure that the language does not become bloated with features that look cool but don't solve real problems. But this very pragmatic mindset may also constrain innovation. Most of the time, programmers discuss the details of their specific problems as seen through the existing language but a language designer should think outside the box. For example, some real problem could be already solved but at a known fixed cost. What if we can come up with a solution that makes it free by changing the language? How do we know if it works? We try it! 

In a thriving community, a lot of people are willing to explore new ideas. Proven solutions that did improve things for users should be added to the official language. This is evolution: survival of the fittest features. But how do we keep the language from becoming too experimental and less industry-ready? We need a broader rule that acknowledges the value of experimentation while "keeping it real".

The right motivation for a change to C³ is when several users are using an experimental feature and see benefits, or, even better, several users independently invent the feature to solve their specific and different problems. An open source compiler and an Internet community are key ingredients for this to work. We must also take care that we do not end up with a sea of incompatible dialects.

All this will happen in the context of much more flexibility than in the case of C++. I intend to make C³ a living language. It does not mean that the language will keep changing constantly but at least it won’t be like the C++ standard committee that tries to set things in stone as much as possible.

⇒ C³’s evolution must be driven by real experience.

Don’t get involved in a sterile quest for perfection.

This is somewhat hard to achieve while at the same time seeking to improve things in as many ways as possible. The escape route is to build a process that will help us evolve forever towards perfection. This process must deliver a working language and make it possible to change things as we discover better ways. Perfection won't be achieved, but we will move towards it. With this iterative process, the "quest for perfection" is transformed into something that is not sterile.

The delicate choice is what to include in each version. It does not need to be perfect but it must deliver some advantage over other languages or previous versions. It must also be open for future evolution. This is the same spirit as agile methodologies like extreme programming: do the simplest thing that could possibly work and then iterate from there when needed. For a language, "working" also means that all the features must work when used together and that they do not block further evolution. The iterations will be needed as long as we discover better ways to fulfill the aims of C³.

 Do the simplest thing that makes C³ better at fulfilling its aims.

C++ must be useful now.

Like C++, C³ is designed to be used in today's environment. This means that it must at least support an average programmer working on an average computer by today's standards. Of course, C++ evolved to track this ever changing target but an accumulation of small changes in the programming landscape makes this evolution harder and harder as time pass. Technologies and techniques that were experimental 25 years ago now become widespread and well known. Today's problems worth solving include the following:

  • It is extremely hard to make effective use of the widely available parallel and distributed hardware resources. The free lunch is over! 
  • Useless coupling between languages and platforms artificially creates difficult technology choices. C# needs a CLR, Java needs a JVM, iOS needs Objective-C.
  • A huge amount of redundant work is done by programmers around the world by lack of generic programming support and insufficient coordination.

C++ has a hard time staying up to date because of its backward compatibility requirement and its 10-year revision process. Armed with new tools like LLVM and PEGs, it is now easier to build a new language that solve those problems than to try to evolve a language that is 25 years old. Since the environment and people constantly change, I think it is natural to take such leaps from time to time. I won't be upset if someone comes out with a replacement for C³ in 25 years! (Even if I'm working hard to make it last 100 years!) The original rule stay the same but "now" is a very different time.

⇒ C³ must be useful now.

Every feature must have a reasonably obvious implementation.

Every compiler vendor has a slightly different opinion of what is the "reasonably obvious implementation". That lead to the incompatible C++ ABIs problem and to the poor support for kernel-level programming. C³ solves this problem by making sure that no core language feature rely on any runtime implementation beyond what is supported by common hardware-level environments (adequately represented by the C abstract machine, for now). It is also made powerful enough to express all the syntactically-supported higher-level abstractions in terms of lower-level ones.

Not all desirable high-level functionalities are trivial to implement but the people who needs them are often willing to put effort and time to implement a complex solution. They usually come up with an ad hoc solution that solve a subset of the general problem. One of the goals of C³ is to make sure that such effort is not lost to the rest of the community. The specific experiment is useful for its users and can be a great starting point for a general solution. Requiring "reasonably obvious implementation" assumes that we start from scratch every time we want to support a new environment. It is not the case in C³.

Added flexibility is often a source of added complexity for the user. We solve this problem by providing reasonable defaults for all the customization points. The choice of the defaults will be made by the language designer at first but will increasingly rely on the community as it matures.

⇒ Provide the best available implementation as a default for every feature.

Always provide a transition path.

Many new features were added to C to create C++. Later, even more features were introduced in the different versions of C++. Those features were often better versions of facilities already there. For example, "std::cout" is a replacement for "printf", "std::string" is a replacement for the manual manipulation of char arrays, exceptions are a better tool to handle errors then error codes and non-local gotos using "longjmp", and new-style casts replace C casts. To provide a transition path, Bjarne Stroustrup chose to keep backward compatibility at the syntax level almost at all cost. The simpler syntax already used by the original features could not be used for the new ones since old code needed to continue working, unchanged.

C³ takes a different path. In order to always have code that is as simple as possible (but no simpler!), the latest version will always have the benefit of the best syntax. You won't be able to compile old code as is, but it's not important as long as there is a way to automatically convert it to the new form without losing any semantic. That means that the new version of the language must support 100% of the old semantics with a reasonable new syntax. (Tracking the language version of some code fragment could become a problem but it should be handled by the environment)

For example, let's say that we have a fake version of C³ in which int variables are not automatically initialized, like in C++. The next version of C³ of course introduce such initialization and old code must be updated. Since the old syntax now means something different (default initialization to 0), we introduce the new “undef” keyword.

code

old meaning

new meaning

1

2

3

int a = 0;

int a;

int a = undef;

a is 0

a is undefined

error

a is 0

a is 0

a is undefined

The role of the updater is to keep the meaning of the definition exactly the same. In the table above, line 2 must be changed to line 3.

Only the future will tell us if we will often need this facility. At least, we have a plan that won't introduce syntax that is both very weird and commonly used. The goal is the same as the original rule: don't have programmers that are stuck with old code that can no longer be used. Thus, the rule stays the same:

⇒ Always provide a transition path.

C++ is a language, not a complete system.

From the perspective of runtime execution, C³ follows this rule even more than C++. The C³ system can generate executable code that can run inside any environment, as long as the necessary back-end exists. There is absolutely no core language feature that relies on the presence of any runtime support beyond hardware. Direct interaction with various systems is possible. C³ has the potential to reproduce the output of any other language in any format. At its core, C³ defines a common vocabulary of variables, values, objects, functions, and messages and a mean to define their behavior.

On the other hand, it is my personal goal to provide a “whole product” to programmers of the world. The programmer that wants to create a C³ program just need to go to http://c3lang.org. She does not need to shop for any missing parts. From there, she can have a complete and integrated development experience. The question is: what part of this is the language and what part is the IDE?

The line is a little more blurry in C³ than in C++. For example:

  • Arbitrary programs can be run as metaprograms inside the compiler environment.
  • Code can refer to source control revisions and branches.
  • Wiki-style documentation and discussions are linked to specific code.

C³ does not impose any runtime system, but it features a very rich development system. It is complete in the sense that, with default settings, no other tool is necessary to create programs.

⇒ C³ is a language, not a runtime system.

Provide comprehensive support for each supported style.

By designing a language from scratch, I'm able to redefine what is part of the much smaller core language and what goes in the “standard” library. From the minimalist point of view of the core language, “comprehensive support” means three things: identify the core abstractions, support them with syntax and provide hooks for user-defined implementations.

The first step is extracting a common vocabulary for the core abstractions needed for each supported style. The next step is designing syntactic constructs for each one and making sure that they can interact with each other in an orthogonal way. These two steps are enough for the user of these abstractions. For the implementation, they are also sufficient when there is a direct mapping to the environment. A more complex implementation can also be provided by the compiler but it should be considered as a temporary solution. The ultimate support is when the implementation can be defined in C³ code. From the point of view of the user, there must be no difference between native, compiler-supplied, and user-defined abstractions.

Different kinds of support can exist for the same abstraction mechanism. For example, a generic function can be implemented with dynamic dispatch at runtime or be polyinstantiated at compile-time. The runtime strategy for dynamic dispatch could be user-defined (using, for example, metaprogramming to add vtable pointers as hidden parameters). It is probably less efficient than the default polyinstantiation supplied by the compiler (similar to C++ template instantiation mechanism), but can have other desirable properties like smaller code size. The choice is in the hands of the user.

⇒ Provide comprehensive and customizable support for each supported style.

Don’t try to force people to use a specific programming style.

Every programmer has a simple goal: get the job done. The trade-off between a reusable or elegant solution and one that works "right now" must be made by each programmer in the given circumstances. Lack of knowledge or experience can be a problem but we should not let this prevent programmers to get their job done, regardless of how poor the style is. If a programmer does not yet understand a style, it is much better to allow him to use a less elegant style that at least he understand. Of course, providing learning material and refactoring tools is also important to enable programmers to help themselves.

⇒ Don’t try to force people to use a specific programming style.

Design support rules

Support sound design notions.

Like Bjarne Stroustrup, my aim is to raise the level of abstraction in programming. My focus is less on the specific class construct and I recognize different kinds of type as a useful design notion, but my goal is definitely the same. I want to support an enhanced continuum of styles consisting of low-level programming, data abstraction, object-oriented programming, generic programming, functional programming, and metaprogramming.

⇒ Support sound design notions.

Provide facilities for program organization.

To paraphrase Stroustrup: “Compared to C++, C³ helps organize programs that are easier to write, read, and maintain”. They can be organized in modules at a smaller granularity with types, functions, etc. (instead of managing text files or name spaces) and shared at a much larger scale using the Internet. This is achieved by blurring the limit between program and library. Every sub-component that emerges from top-down design has the potential to be reused for future bottom-up designs.

However, contrary to Stroustrup, I don’t consider computation solved by C. At least, not when using high-level abstractions. Fundamental concepts like regular types were not known when C was developed and it is important to revisit the basic constructs so that they follow simple rules. That way, we end up with a more powerful language without adding complexity.

⇒ Provide facilities for program organization.

Say what you mean.

Having a way to distinguish between data types and object classes at the language level is a step further towards this goal. Please note that the Java/C# world got this concept completely backwards. For example, if I want to express a mathematical function in C++ or C³, I create... a function. If I want to do the same in Java, I have to create some dummy class and declare a static method... Direct mapping from concepts to language constructs is always better.

⇒ Say what you mean.

All features must be affordable.

Determining what is affordable is a very context sensitive question and should not be decided in advance by the language designer. Basic computer science concepts like branching and procedures are cheaply implemented. More complex but still fundamental concepts like concurrency and closures require additional runtime support. If an implementation exists, deciding if it is affordable is left to the leader of a specific project to decide. C³ allows users to disable or enable features on a case-by-case basis. If most people would consider the feature unaffordable, it will be disabled by default, but still be available.

This rule has no equivalent for C³ but it is subsumed into the two rules that follow the next paragraph.

It is more important to allow a useful feature than to prevent every misuse.

Like C++, the default C³ setup protects against accidents but allow deliberate violations. This protection is often enough because C³ cleanly separates between abstractions provided by the environment and the ones defined inside of it. Inner abstractions can be explicitly trespassed if necessary because, for the knowledgeable programmer, what matters is what gets across to the environment.

But C³ also supports a new way to look at this problem. It allows programmers to define a new “native” environment. Such an environment defines a set of native abstractions just like the hardware one. When programs are written within this environment, there is absolutely no way to bypass those abstractions. The same rule as with the hardware environment applies here: abstractions built in “user-space” can be trespassed when necessary.

The fact that we can explicitly define a safer environment means that we can define the hard limits ourself. This can help self control or constrain others. We live in a world where we want to empower more people to program computers without a prohibitive entry barrier. The added runtime cost can be a trade-off for increased security. It is a reasonable business decision in some cases. The key thing here is that there is usually a technical person that makes such decisions; it is neither the language designer nor the end-user.

A common pattern in the projects I worked on is a separation of responsibilities: a group defines an API that talks directly to the machine while another group is free to concentrate on higher-level concerns. An interesting benefit of C³ is that generic programming allows all those people to share a great deal of source code, regardless of the underlying native abstractions.

⇒ Don't set policy.

⇒ Provide a mean to set policy.

Support composition of software from separately developed parts.

C³ supports this concept for external necessities like creating a DLL for an existing system or classes consumable from another language. However, it takes a completely different strategy for entities entirely made out of C³ code. Its multi-stage compilation strategy make sure that it is able to perform whole program optimization at high speed. It completely replaces the whole "compile/link/run" system with a completely integrated system for which the user only needs to know "run". There is centralized and scalable support for sharing the cost of software building. It enables a new paradigm where build time is no longer an issue. The life-long program optimization strategy pioneered by LLVM is applied to C³ and its intermediate form, C³VM. We are no longer in a separated world! Online collaboration enable parts to be combined in new unprecedented ways. Complete access to the source code is better for documentation, optimization and collaboration.

 Unleash large-scale collaboration.

Language-Technical Rules

No implicit violations of the static type system.

This is much easier to achieve in a new language. The static type system in C³ is more than a facility to ensure the safety of programs. It is enabling much simpler programs to exist. "Modern" object-oriented programming languages often restrict user-defined types to heap-allocated entities that can only receive a set of constrained or unconstrained messages. This is very restricted and certainly does not support them as much as built-in types. Types in C³ are the access key to the power of the hardware. Abstraction is good but it ultimately map to some hardware features. Types gain their power from these roots. Since this power is sometimes quite raw, additional security is given by building higher-level abstractions on top of it. But, as in C++, sometimes raw power is necessary and explicit violations may be necessary. If you don't want them, simply use a safer native environment. Explicit violations can be easily prohibited by using a type safe environment, i.e. one that does not allow free usage of pointer arithmetic.

⇒ No implicit violations of the static type system.

Provide as good support for user-defined types as for built-in types.

Once again, this is a rule that C³ follows much better than C++. All types are regular by default and object-oriented versions are built on top of value types. The interface of basic types are defined with the same facilities that a user would use to define its new types. Full optimization of common sub-expressions and partial evaluation is made possible because of enhanced constructors and destructors, serialization functions, and more.

 Provide as good support for user-defined types as for built-in types.

Locality is good.

Yes! C³ goes further with local functions, more complete support for local types, modules, and more. Of course, a local entity that could be useful as a global one must be extracted from its context when possible (when no real dependencies exist). Since all dependency information is computed by the compiler and made available, refactoring tools that do exactly that can be built.

⇒ Locality is good.

Avoid order dependencies.

It is surprising to see this among the C++ design rules since it's not followed very well. For example, putting declarations in a different order may often produce a different result!

C³ completely get rid of the header files system. Global entities live in a completely unordered set. No forward declaration is necessary to reference a constant object that appear at the same scope level or above.

Functional programming aficionados would say that this rule should be followed to a point where imperative programming itself is outlawed. I don’t go that far because, at a local scale, following execution in linear order is easier.

⇒ Avoid order dependencies.

If in doubt, pick the variant of a feature that is easiest to teach.

I don't claim that I can remove complexity from the world of programming. But it is important to explore an idea that might lead us to language constructs that are easier to teach: intuition.

Intuition is a great source of inspiration. If something is intuitive, it means that it resonates with the way we think. People often don't have the theoretical background necessary to understand unfamiliar constructs. They slowly understand things by associating them with other things they already know. Intuitive constructs make this easier.

If something is very intuitive, it does not need teaching. Something that does not need teaching is better than something that is easy to teach. Things that really need some teaching should be broken down in concepts that are easy to describe. Many small features are easier to teach than complex combinations. Combinations are best left to discover by practice because context is necessary for comprehension.

All this can lead to a more manageable complexity. This is why I modified the rule to be about individual features and to be more uniformly applied.

⇒ Each individual feature must be easy to teach.

Syntax matters (often in perverse ways).

Syntax forms a common vocabulary for programmers to share. This is very important. Carefully crafting a syntax that looks familiar, is terse, and provides support for the comprehension of new concepts is a very important psychology exercise. I identified false rules that people often extrapolate from the complex syntax of C and C++ and built the C³ syntax around them.

Example #1: a variable declaration is its type followed by its name. Most programmers know it's not always the case in C and C++ but they often use typedefs to make their declaration follow this simple form.

Example #2: new-style casts are template functions. It's not true in C++. For example, you can't overload them and they are reserved keywords.

Both examples are the real rules that apply in C³.

⇒ Syntax matters.

Preprocessor usage should be eliminated.

The preprocessor is mainly used in C++ for two things: header file include guards and conditional compilation. I really don't understand why both issues still don't have a solution in the language itself. C and C++ are the only remaining languages that rely on an separate preprocessor. All this come from the initial design of C as a really simple system that built on existing tools. C³ does not have header files and its metaprogramming facilities takes care of all conditional compilation needs and more.

Bringing this rule further means eliminating reliance on any external tool. Other tools like makefiles, source control, documentation systems, etc. should be implemented within the language as much as possible. Makefiles can be completely made obsolete by language features. Others can be implemented through an interface that is easier to handle than raw text. For example, source code generators could be implemented as metaprograms.

 Don’t rely on external tools for essential features.

The standard library is specified and implemented in C++.

Since C++ has always been designed to implement data structures and algorithm directly in the language, it is only natural that its designer wants the standard library to be a proof of its capabilities. This is also easier for the implementer. However, it’s not completely true. Parts of the C library (which is a subset of the C++ library) are usually written in assembler (like memcpy for example). Also, nothing in the standard requires that the implementation is in C++. Its interfaces are specified in C++ but the implementation could be built in the compiler. Given the poor modular programming support in C++, this could probably provides better compiler performance.

I consider the spirit of C++ regarding this rule to be good. However, the resulting unreadable code in many standard library implementations is a sign that something went wrong. Moreover, C³ doesn’t have a “standard library” in the traditional sense. “The library” contains code built by the community. It’s written in C³ and modules are ranked according to their quality. The best ones become a de facto standard. Therefore, I need more general rules:

⇒ All facilities have an interface in C³.

⇒ All facilities not directly supported by the environment are implemented in C³.

Low-level programming support rules

Use traditional (dumb) linkers.

Being able to generate object code that can be linked using old school linkers may be a secondary goal but it is not fundamental. LLVM introduced the much better concept of keeping the compiled code in a optimizable form as long as possible and that's the inspiration for C³.

(There is no equivalent rule for C³.)

No gratuitous incompatibilities with C.

Since compatibility with any previous language is not a goal of C³, this rule does not apply. However, binary compatibility with various real world systems is a goal, including common C ABIs (and even maybe some C++ ABIs).

(There is no equivalent rule for C³.)

Leave no room for a lower-level language below C++ (except assembler).

Yes, except for the exception. Full and direct access to the underlying machine must be possible without resorting to another language. In high-level only languages, at some point you need to switch language and the resulting glue code is usually the most horrible and boring code. At an extreme level, this rule implies that a C³ mapping of the target machine instruction set can be made available and used directly.

⇒ Leave no room for a lower-level language below C³ (including assembler).

What you don’t use, you don’t pay for (zero-overhead rule).

This is a very important rule that makes C++ unique among other high-level languages. It makes sure that added flexibility is never bought at the price of overhead when we don't need it. C³ might be the only successor of C++ to follow this rule.

⇒  What you don’t use, you don’t pay for (zero-overhead rule).

If in doubt, provide means for manual control.

Even if I know that manual control is not needed, I will always make sure that it is available to guarantee to my user that they won't get stuck on any abstraction that don't work for any reason. This is the law of the leaky abstraction: at some point, any abstraction fails to completely hide and replace the lower-level abstraction that it encapsulates. Sooner or later. (Yet, abstraction is so helpful!)

The rule is changed just to remove any reliance on "doubt".

⇒  All implementation choices can be made by the user.

There is a direct mapping of C++ language constructs to hardware.

This is another rule that C³ embraces and expands. While C++ is stuck in a world of execution stack and pointers, C³ will be open to accept future hardware or software innovations.

⇒ C³ allows mapping to any native environment.

]]>
tag:blog.c3lang.org,2013:Post/872401 2015-06-22T16:54:05Z 2015-06-22T20:21:24Z Aims and Rules of Thumb for the Design of C³

(This is from my old blog, it was initially posted in 2011.)

Aims 

C³ makes programming more enjoyable for serious programmers.
 
C³ is a general-purpose programming language that supports
– low-level programming
– data abstraction
– object-oriented programming
– generic programming
– functional programming
– metaprogramming
 
General rules
 
C³’s evolution must be driven by real experience.
Do the simplest thing that makes C³ better at fulfilling its aims.
C³ must be useful now.
Provide the best available implementation as a default for every feature.
Always provide a transition path.
C³ is a language, not a runtime system.
Provide comprehensive and customizable support for each supported style.
Don’t try to force people to use a specific programming style.
 
Design support rules
 
Support sound design notions.
Provide facilities for program organization.
Say what you mean.
Don't set policy.
Provide a mean to set policy.
Unleash large-scale collaboration.
 
Language-Technical Rules
 
No implicit violations of the static type system
Provide as good support for user-defined types as for built-in types.
Locality is good.
Avoid order dependencies.
Each individual feature must be easy to teach.
Syntax matters.
External tools usage should be eliminated.
All facilities have an interface in C³.
All facilities not directly supported by the environment are implemented in C³.
 
Low-level programming support rules
 
Leave no room for a lower-level language below C³ (including assembler).
What you don’t use, you don’t pay for (zero-overhead rule).
All implementation choices can be made by the user.
C³ allows mapping to any native environment.

]]>