fut icon indicating copy to clipboard operation
fut copied to clipboard

7 ideas to improve ci (in my opinion)

Open edrozdikov opened this issue 2 years ago • 4 comments

First of all, I want to tell you that this is best language I saw.

  1. Using break in switches. I have quite big experience working with custom programming languages and can assure that leaving break as default behavior of each case lead to much easier and readable code. switch (someValue) { case 1: DoFoo(); case 2: DoBar(); case 3, 4, 5: DoCar(); case 6..9: DoSomethingSmart(); } Some useful thing could be automatically generated default: assert false, “switch someEnum unsupported case 10”. If I don’t want to have assert, I’d just add default: ;. I guess 99% of all switches had expected behavior to cover all cases.

  2. Using one line declarations int a, b, c; // syntax error It is very handy when working with big scripts, projects with lot of logic and complex algorithms. I’d recommend to leave this behavior or move it to some codestyle settings.

  3. in keyword (this is minor idea) foreach ((string k, int v) in dict) using : as alternative to in keyword

foreach (Item item : ItemsList) or even foreach (item : ItemsList) where item type is type of Lists elements

First one is also very good.

  1. WriteLine is used 99% more often than Write in my experience. Having Write() for WriteLine is more simplier, while making some other name for case when we don’t want to break line.

  2. Having some kind of sugar for easier properties. Closer to c#. class member int width; int GetWidth() { return width; }; void SetWidth(int value) { width = value; };

allow to use something similar to int width; {get; set;} int width; {public GetWidth() : get; public SetWidth(int value) : set}

int height; public int GetHeight() : get height; public int SetHeight(int value) : set height;

keywords get set to generate default code.

Personally I think => sugar syntax isn’t beautiful and doesn’t work for Setters.

  1. Fill memory with nulls for uninitialize variables; This can be useful and make code clearer from my experience. String() someString; // ==“” when read float countOfEvenInArray; … countOfEvenInArray++;

  2. Idea of having codestyle defines. Allowing to have one rule inside one project but different rules for different teams and codestyle guides. e.g. if some team prefer to use ? optional or having break; as default case behavior they can put it into cicode.config file UseBreakAsDefaultForSwitchCases = true; AllowGettersSugar = false; UseStrictSpaceIntendation = false; // true for python guys NullifyDeclarations = true; And so on…

Yes, it has negative side that ci code starts to work/feel little different; but it will be still compatible if user specifies code file codestyle.

e.g. using projectxcodestyle.config; at the start of the code;

positive side is that each team can tailor your language to their needs and individual case.

Codestyle rules should be very limited and cherry-picked, but having default break on switch and one line var declaration is very handy;

Just wanted to share my thoughts, cause I like what you are doing.

edrozdikov avatar Oct 24 '21 19:10 edrozdikov

Thank you! That's actually more than 7 ideas!

1a. Implicit break at the end of every case.

I considered this at an early stage of Ć. It greatly reduces verbosity of the switch statement. The default fallthrough is a bad thing and already not supported by Ć. The problem is that if you have vast experience in C, C++, C#, Java or OpenCL, you are used to breaks and they feel natural. A switch without break looks weird. So I chose to have familiarity (aka POLA) over conciseness.

1b. Comma-separated case values.

This looks good and might be added. What languages support that syntax?

1c. Integer range in case.

How to translate a broad range if the target language doesn't support it? To thousands or millions of cases? Also, do you have real-world examples where it would be useful?

1d. Checking there are cases for all enum values.

I like this idea. cito could also report this at compile time.

2. Comma-separated variable definitions.

This only makes the code less readable and polluted with unused variables. I'd like to promote a coding style where a local variable is defined when it is first initialized, that is:

int foo = GetFoo();
...
int bar = GetBar();
...
int quux = GetQuux();

instead of:

int foo, bar, quux; // the scope is unnecessarily extended
foo = GetFoo();
...
bar = GetBar();
...
quux = GetQuux();

From my experience comma-separated variable definitions often lead to unused variables, because it's unclear where the variable is initialized and if at all.

3. Different foreach syntax.

None of the target languages uses the syntax you proposed:

for (Type e : collection) // C++, Java
foreach (Type e in collection) // C#
for (const e of collection) // JavaScript, TypeScript
for e in collection // Python, Swift

I chose to follow C#'s syntax in this and many other places. I want something familiar to programmers.

4. Spelling WriteLine as Write.

Write and WriteLine are meant to be primitive debugging tools rather than production Ć code. The syntax resembles C#'s.

5a. C#-style get/set.

Yes, getters and setters are missing. I will consider adding them to Ć. One problem is that the names of the properties often collide with the names of the backing field names.

5b. Against the => short return syntax.

I'm also not a fan of =>, but that's what C# uses, so again I adopted it. Previously Ć supported return without the method body braces.

6a. Default-initialize variables.

Local variables are uninitialized by default in all the target languages. Their compilers have rules to detect that, sometimes codified in the language standard (Java). I think that's good enough.

6b. Default-initialize fields.

I don't have a strong preference for whether fields should be initialized by default.

  • C and C++ leave them uninitialized
  • C# and Java default-initialize
  • JavaScript and Python have no syntax to define a field without initializing it
  • D (currently not targetted by cito) default-initializes, but with NaNs for floats

I understand that having uninitialized fields is error-prone, but C and C++ programmers tend to care about every CPU cycle, so at least it should be possible to have explicitly uninitialized fields, e.g.

int Foo = ?;

7. Dialects.

This will make the language harder to learn and understand. I'm not aware of programming languages introducing dialects on their first versions. C# added explicitly nullable references after nearly two decades of being in production.

pfusik avatar Nov 09 '21 19:11 pfusik

Regarding getters and setters, may I suggest something like ActionScript? I don't know how popular this is with other programming languages, but in that language, they can be defined like this:

var _hello;
function get hello()
{
  return this._hello;
}
function set hello(value)
{ // obviously this getter/setter pair is a bit basic!
  this._hello = value;
}

With this definition, this syntax is used:

myClass.hello = "Hi!";
trace(myClass.hello);

as in C#. This could be reworked into something like:

int myVariable;
public string() get myVariable() {
  return $"Variable: {this.myVariable}";
}
public void set myVariable(int value) {
  if (value < 10) {
    this.myVariable = value;
  }
}

I don't think it's necessary to worry about names overlapping - from within the getter/setter, any reference to this.myVariable should be to the underlying value, and any other reference should pass through the getter/setter. If this is unacceptable, consider just adding an underscore to the beginning of the underlying value - this is common from what I've seen. You could also keep the syntactic sugar for getters:

int myVariable;
public int get myVariable() => this.myVariable;

and I think this could be quite sensibly extended to setters:

public void set myVariable(int value) <= value + 5;

or go the C# route:

public void set myVariable(int value) => this.myVariable = value - 2;

Consider this:

string currentDay = "Monday";
string currentMonth = "January";

public string() get date() => $"Today is a {this.currentDay} in {this.currentMonth}";
public void set date(string date) {
  switch (date) {
    case "Monday":
      this.currentDay = "Monday";
      break;
// ...

    case "Sunday":
      this.currentDay = "Sunday";
      break;

    case "January":
      this.currentMonth = "January";
      break;
// ...
  }
}

// later on
MyClass() test;
Console.WriteLine(test.date); // Today is a Monday in January
test.date = "Tuesday";
test.date = "March";
Console.WriteLine(test.date); // Today is a Tuesday in March

(I'm aware this code isn't the greatest, I had to come up with something off the top of my head with very little experience using the language yet) This also shows off a neat feature of the way ActionScript handles setters specifically: it is very easy to make a set of several variables behave as if they are one variable, by using a setter and getter to define a single interface. There are much better use cases than this example, such as for localisation, but again, I came up with this very quickly.

Jhynjhiruu avatar Jan 26 '22 17:01 Jhynjhiruu

Regarding property syntax; something like C# public T foo {get {}; set {};} is probably good enough, and would translate as well as or better than explicit getters and setters; C#, Python, JS/TS and Swift would all get to use their property syntaxes (for Python and JS, where it doesn't change the meaning of the code, properties with default getters and setters could just be compiled into fields), and C/C++/Java would get their getter/setter methods as before. Additionally, if name conflicts with the store are an issue, consider something like a magic _storage variable which is generated if and only if it's accessed in the getter and setter (given some name like _fieldName)

Examples (with translation to Python and Java)

class Foo
    public int bar { get; set; };
    public int baz { get => 42; };
    // I think it looks nicer to be explicit about my parameter, although C# does not do this
    public int useStorage { get => _storage; set(value) {
        if value > 3 {
            _storage = value;
        }
    }; };
    public int noStorage { get => this.useStorage; set(value) { this.useStorage = value; }; };
}
class Foo:
    bar: int
    def __init__(self):
       self.bar = 0
       self.__use_storage = 0
    @property
    def baz(self):
        return 42
    @property
    def use_storage(self):
        return self.__use_storage
    @use_storage.setter
    def use_storage(self, value):
        if value > 3:
            self.__use_storage = value
    @property
    def no_storage(self):
        return self.use_storage
    @no_storage.setter
    def no_storage(self, value):
        self.use_storage = value
class Foo {
    private int bar;
    public int getBar() { return bar; }
    public void setBar(int value) { bar = value; }
    public int getBaz() { return 42; }
    private int _useStorage;
    public int getUseStorage() { return _useStorage; }
    public void setUseStorage(int value) { _useStorage = value; }
    public int getNoStorage() { return getUseStorage(); }
    public void setNoStorage(int value) { setUseStorage(); }
}

Apologies for my long example lmao, but that's how I would consider this approach

Starwort avatar Apr 03 '22 09:04 Starwort

  1. Using break in switches. I have quite big experience working with custom programming languages and can assure that leaving break as default behavior of each case lead to much easier and readable code. switch (someValue) { case 1: DoFoo(); case 2: DoBar(); case 3, 4, 5: DoCar(); case 6..9: DoSomethingSmart(); } Some useful thing could be automatically generated default: assert false, “switch someEnum unsupported case 10”. If I don’t want to have assert, I’d just add default: ;. I guess 99% of all switches had expected behavior to cover all cases.

Falling back of clauses in switch are a bad idea, and break doesn't help much. In fact, switch itself is rather not a good construct, I very rarely use it. I much ofer go for maps, dicts or strategies.

  1. Using one line declarations int a, b, c; // syntax error It is very handy when working with big scripts, projects with lot of logic and complex algorithms. I’d recommend to leave this behavior or move it to some codestyle settings.

In my opinion forward declaring variables is also a bad idea. It seams like unnecessary scope expand, and also a sign of too big methods. Some languages actually go into the way of no redeclaring variables, like kotlin.

  1. in keyword (this is minor idea) foreach ((string k, int v) in dict) using : as alternative to in keyword

foreach (Item item : ItemsList) or even foreach (item : ItemsList) where item type is type of Lists elements

I also like for (item in ItemsList) looks better, if there was no type in the loop (like in PHP or Python). But when you have to declare a type (for (Item item : items), like in Java), : is actually more readable in my opinion.

  1. WriteLine is used 99% more often than Write in my experience. Having Write() for WriteLine is more simplier, while making some other name for case when we don’t want to break line.

Only when working for console applications or applications that use standard output. That's not majority of applications, to be honest.

danon avatar May 31 '22 18:05 danon