Data structure: Chained List

Hello World! Here is Switch. Having introduce the bases of programming, it is time to go a bit deeper. This post will start a serie about data structure. Data structure defines solution to manage data set in a program and using the correct structure will greatly improve code performance. Also, a good structure will simplify the implementation of problem resolution.

As a first post around data structure, I will introduce the basic Chained List structure.

Structure description

A chain list build upon a single entry point witch represent the Head of the list. The head stores a value and a reference (pointer) to the next listed elements. It has no index notion and is a first in, last out structure. It means that you will neither have a direct access to a known element of the list without depilating all heads before the elements and the first element to be added will be retrieved last.

Graphical representation of list: [9,1,12]

When defining a list, we have to provide user with basic function to manipulate it. We also like to define more advanced powerful tools.

Essentials feature to provide is a Prepend method to add element to a list. You also have to provide a Head et un Tail function to retrieve the first element and queue of current list head.

For advanced tools, I like to provide a Sort method witch will allow you to reorder list elements and a Map function. The Map function allows you to apply a method to each element composing the list. If I call Map with a times two function on the [9,1,12] list, it should return me the [18,2,24] list.

Pseudo code defintion

Now, let’s propose a pseudo code definition of the list structure. I will introduce a new keywords to our pseudo code: define NAME { Properties list }. It will create a structure named NAME containing specific values. I can associate methods to this structure using func NAME(Variable).Method notation and properties listed in the structure can be accessed through NAME.Property.

Let’s define the basic structure and methods.

define List {
  Head any
  Tail List
}

emptyList = List{} // I expect emptyList to be a global constant.

func NewList() List {
   return emptyList
}

func List(l).Prepend(element any) List {
   return List{
        Head<-element
        Tail<-l
   }
}

func List(l).Pop() any, List {
    return l.Head, l.Tail
}

Our list structure keeps track of its head and tail by relying on an internal propertie Head and Tail. Head is any value you wish to add to the list and Tail the reference to next part. I define an emptyList constant so we can easily identify it. NewList create an empty list where you will be able to add elements. Prepend method takes the value you provide to it and build a new List structure using current List element as Tail. Finally, the Pop method will return you a couple containing the Head value of the list and the full Tail list.

Then let’s simply sort the list. I will propose the basic bubble sort implementation. To bubble sort a list, you need to sort its tail then add the head at the current place in the sorted list. Witch will give something like:

func List(l).SortedAdd(element any, comp function) List {
     if l == emptyList {
         return emptyList
     }

     if comp(element, l.Head) is True {
          return List{Head<-element, Tail<-l}
     }

     return List{Head<-l.Head,Tail<-l.Tail.SortedAdd(element, comp)}
}

func List(l).Sort(comp function) List {
     if l == emptyList {
          return l
     }
     
     sorted = l.Tail.Sort()
     return sorted.Add()
}

Finally, let’s define the Map method. Map will go through each Head element in List, apply provided method and build a new list element using this newly computed value and the application of Map to its Tail.

func List(l).Map(fn function) List {
     if l == emptyList {
         return emptyList
     }

     return List{Head<-fn(l.Head),Tail<-l.Tail.Map(fn)}
}

Now let’s code it in Go.

Test definition

We will start by defining a code process for our list. We know that we will create a List structure witch will contained a Head of type interface{} (interface is a neutral type in Go) and the Tail will contained a recursive call to List type. For this structure, we wish to define an Empty value and an asserter for emptiness. We also wish the constructor of list type to provide an Empty list. A first test would then be:
– When I create a new list
– I expect it to be an Empty list

func TestNewList(t *testing.T) {
   Convey("When I create a list", t, func() {
      newList := list.New()
      Convey("I expect it to be empty", func() {
         So(newList.Empty(), ShouldBeTrue)
      })
   })
}

Then we would like to add element to a list. After adding an element, I should ensure the updated list is not empty, as a Head value equal to provided data, and has a tail equals to initial list. The test protocol would be:
– Given I have an empty list
– When I add an element X
– List should not be Empty
– Head should equal X
– Tail should be empty


– Given I have a list L
– When I add an element X stored as newL
– newL should not be Empty
– newL Head should equal X
– newL Tail should equal L

Convey("Given I have ", t, func() {
   Convey("an Empty list", func() {
      l := list.New()
      Convey("When I add an element X", func() {
         newL := l.Prepend("X")

         So(newL.Empty(), ShouldBeFalse)
         So(newL.Head, ShouldEqual, "X")
         So(newL.Queue.Empty(), ShouldBeTrue)
      })
   })

   Convey("a list", func() {
      l := list.New().Prepend("X").Prepend("Y").Prepend("Z")
      Convey("When I add an element X", func() {
         newL := l.Prepend("A")

         So(newL.Empty(), ShouldBeFalse)
         So(newL.Head, ShouldEqual, "A")
         So(*newL.Queue, ShouldResemble, l)
      })
   })
})

Then we can define test protocol for Pop:
– Given an Empty list L
L.Pop() should return nil, EmptyList

– Given a [3,4,5] list L
– L.Pop() should return 3, [4,5] as H, L2
– L2.Pop() should return 4, [5]

Convey("Given", t, func() {
   Convey("an Empty list", func() {
      l := list.New()
      h, q := l.Pop()
      So(h, ShouldBeNil)
      So(q.Empty(), ShouldBeTrue)
   })

   Convey("a known list", func() {
      l := list.New().Prepend(5).Prepend(4)
      l2 := l.Prepend(3)
      h, q := l2.Pop()
      So(h, ShouldEqual, 3)
      So(q, ShouldResemble, l)
      h2, _ := q.Pop()
      So(h2, ShouldEqual, 4)
   })
})

Now, we should ensure Map function will behave.
– Given an Empty List L
– L.Map(func(e interface{}) {return e}) should return Empty List

– Given a single element list L = [“hello”]
– And a function addWorld witch add ” world!” to string interface
– When I call L.Map(addWorld)
– Then I should have [“hello world!”] list

– Given a list L = [“hello”, “my”, “new”]
– And a function addWorld witch add ” world!” to string interface
– When I call L.Map(addWorld)
– Then I should have [“hello world!”, “my world!”, “new world!”] list

func TestList_Map(t *testing.T) {
   Convey("Given ", t, func() {
      Convey("an Empty list", func() {
         l := list.New()
         Convey("when I apply Map function", func() {
            newL := l.Map(func(e interface{}) interface{} { return e })
            So(newL.Empty(), ShouldBeTrue)
         })
      })

      l := list.New()
      addWorld := func(e interface{}) interface{} { return e.(string) + " world!" }

      Convey("a single element list", func() {
         l = l.Prepend("hello")
         expectedL := list.New().Prepend("hello world!")
         Convey("when I apply Map function", func() {
            newL := l.Map(addWorld)
            So(*newL, ShouldResemble, expectedL)
         })
      })
      Convey("a  list", func() {
         l = l.Prepend("new").Prepend("my").Prepend("hello")
         expectedL := list.New().Prepend("new world!").Prepend("my world!").Prepend("hello world!")
         Convey("when I apply Map function", func() {
            newL := l.Map(addWorld)
            So(*newL, ShouldResemble, expectedL)
         })
      })
   })
}

Finally, we should safe the sort function. Again, sort function should not alter empty or single element list. Also, given a sorted list, the list should stay sorted and given a randomly ordered list.

func TestList_BullSort(t *testing.T) {
   Convey("Given a list", t, func() {
      tmpList := list.New().Prepend(1).Prepend(4).Prepend(6).Prepend(9).Prepend(3).Prepend(5).Prepend(2).Prepend(10)

      expectedSorted := list.New().
         Prepend(1).Prepend(2).Prepend(3).Prepend(4).Prepend(5).
         Prepend(6).Prepend(9).Prepend(10)

      l := &tmpList
      comp := func(a, b interface{}) bool {
         return a.(int) > b.(int)
      }

      Convey("empty or single element, should stay as his", func() {
      })

      Convey("not sorted, I should be able to sort it", func() {
func TestList_BullSort(t *testing.T) {
   Convey("Given a list", t, func() {
      tmpList := list.New().Prepend(1).Prepend(4).Prepend(6).Prepend(9).Prepend(3).Prepend(5).Prepend(2).Prepend(10)

      expectedSorted := list.New().
         Prepend(1).Prepend(2).Prepend(3).Prepend(4).Prepend(5).
         Prepend(6).Prepend(9).Prepend(10)

      l := &tmpList
      comp := func(a, b interface{}) bool {
         return a.(int) > b.(int)
      }

      Convey("empty or single element, should stay as his", func() {
         l := list.New()
         So(l.BullSort(comp), ShouldResemble, &l)

         l = list.New().Prepend(1)
         So(l.BullSort(comp), ShouldResemble, &l)
      })

      Convey("not sorted, I should be able to sort it", func() {
         l = l.BullSort(comp)
         So(*l, ShouldResemble, expectedSorted)
      })

      Convey("sorted, I should be able to sort it", func() {
         l = l.BullSort(comp)
         l = l.BullSort(comp)
         So(*l, ShouldResemble, expectedSorted)
      })
   })
}

Then, we just have to complete our code until test pass.

Implementation

package list

// List represents a simple chained list.
// Head provides the element
type List struct {
   Head interface{}
   Tail *List
}

// MapFunc abstracts Map method arguments type
type MapFunc func(element interface{}) interface{}

// New initialize an empty list
func New() List {
   return List{Tail: nil, Head: nil}
}

// Empty asserts list is an empty list.
func (l List) Empty() bool {
   return l.Head == nil && l.Tail == nil
}

// Prepend adds elements to list head.
func (l List) Prepend(elem interface{}) List {
   return List{
      Head: elem,
      Tail: &l,
   }
}

// Pop recovers head and queue
func (l List) Pop() (head interface{}, queue List) {
   if l.Empty() {
      return nil, l
   }
   return l.Head, *l.Tail
}

// Map applies a function to each element of a list
func (l *List) Map(fn MapFunc) *List {
   if l.Empty() {
      return l
   }

   return &List{
      Head: fn(l.Head),
      Tail: l.Tail.Map(fn),
   }
}

// AddSorted adds an element to sorted list at the right place.
// /!\ function will not work correctly on non sorted list.
func (l *List) AddSorted(e interface{}, comp func(interface{}, interface{}) bool) *List {
   if l.Empty() {
      return &List{Head: e, Tail: l}
   }

   if comp(e, l.Head) {
      return &List{Head: e, Tail: l}
   }

   return &List{Head: l.Head, Tail: l.Tail.AddSorted(e, comp)}
}

// BullSort sorts a list
func (l *List) BullSort(comp func(interface{}, interface{}) bool) *List {
   if l.Empty() {
      return l
   }

   sorted := l.Tail.BullSort(comp)

   return sorted.AddSorted(l.Head, comp)
}

You will find all code written in this article on github: https://github.com/switchelven/data-structure

That’s all folks. See you later.

Testing

Hello, here is Switch. Today, we will introduce program testing.

When we code, we make lots of fonctions witch have different impacts and can be broken for various reason (forgetting an assignment, wrong calcul, messed up pointers, global variables broken and so on). Those function are created to respect known objectives witch should be ensured by the methods. If we want to ensure our code respect the objectives, we need to test it.

Test solutions

At the beginning, you will be tempt to only relies on manual testing. You run your code until it works, adding debug points with logs and so on. The shortcoming of this method is quite obvious. When you code base is very light and you have a small number of features, you can easily cover all required test case by yourself. But it will be a nightmare when you start working on real applications. That leads to the apparition of automated test. Those test can be written on different level and has various usages. Though they run automatically, they have to be written by hand so they come with a time cost. If correctly managed, this cost can be nullify and it can make a team faster.

Testing is generally separated using levels:

  • Unit test: low level test witch call a single function and ensure it works as expected.
  • Integration test: low level test witch test a function and ensure it behaves as expected with external services. (To abstract Unit vs Integration, we can say a Unit test does not rely on external services while Integration does but they are the same kind of test, isolating a specific feature and ensuring it works).
  • Functional/API test: Global test relying on user callable endpoints. They ensure a feature provided to user will behave as it should.
  • End to End test: Test using the user interface directly. They ensure the final products correctly mapped function to user interface components.

There are a lot of test solutions build for each of those levels. Unit test are generally integrated directly with the langage (you use C code for C unit test, Go for Go, etc … ). For higher level test, frameworks usually try to abstract the langage behind and allows to express directly features to test (like gherkin witch let you define steps you will map in your own langage).

Test Driven Development

With the popularisation of testing and testing being more and more present and powerful, we have a new way to develop in sight. Instead of coding then write test to ensure code will not broke, we will first write a test then implement required code to make it pass.

When you are used to do testing after writing code, the step is hard to take. If you are in this case, you should try to write only thing you can know without knowing the implementation to create your test and divide test case in 3 steps: Initialize, Do, Ensure. This is a gage that was given to me while looking for solutions to enforce TDD so I also pass it to you.

If you are new to development, I would recommend getting used to it, and that’s what I made this article for.

So, how does TDD works and why should use it ?

There are mostly benefits to use TDD. It will helps you to minimalise the development. As you start by describing what your code must do, you will clarify your idea and fix the behaviour of your code before implementing. So when you implement, you know you can stop when the test pass.

You will write clearer test. As your test does not describe the implementation but the expectation, it will be clear to anyone with a minimal knowledge of code and your product.

Writing good tests

I do not own absolute knowledge on what a good test is. No one does. But some points seems to be proven useful

  1. Validate border cases, at least one global case, and known errors
  2. Mocks only relevant thing you own (a mock is a replacement for a part of the code where you enforce result to be return by code). If you use external services, you should use a mock server witch will return a known valid response taken from official server than mocking the client part to return what you would want.
  3. Clearly separate the initialisation, actions and expectation part.
  4. Can be used as documentation for developers. If you wrote a clear test correctly commented, any developer should understand how the code works and behave just from the test case.

That’s all for today. From then on, every article I will write including code part will describe test files first and be developed using TDD method. I hope you will adopt the same behaviour though I will never condemn those who do not goes through it.

Memory Usage

Hello World! Here is Switch. Today, we will quickly cover the notion of memory management in computer programs. I will cover notion from pointers to data scope and use program analysis to explain some points.

Introduction to memory management

This post doesn’t aim to cover the overall complexity of memory management witch requires to know how to access specific part of memory, choose between different memory providers and so on. It will instead focus on memory optimisation within a code by ensuring variables are correctly scoped avoid useless duplication.

There are lot of way to manage memory in a program. Nowadays, most langages use the notion of Garbage Collector to ensure memory taken by variables is released when it has no more usage but older langage such as C will require you to do it yourself.

Optimising memory usage through a program requires to correctly identifies the life cycle of values. When do I create it, witch function will use it and when will it be no more used. You also need to be aware of the weight of variables you are going to manipulate. Depending on your performance objectives, passing a full copy of variables to sub function can be a blunder.

Let’s look at this code:

a = 10
b = 15
c = a + b
d = a - b
e = c * d 

There are a lot to say. Though this code is quite simple, it is under optimised. Let’s say we cannot cumulate operation (I cannot directly write (a+b)*(a-b)). We could still save at least a variables.

Variables a and b are used to compute c and d so they need to be alive for c and d but e use c and d, not a and b so we don’t need a new variables here. It makes things more readable but take useless space. We could put c*d response to a or b to save a space. We can make the same analysis for d variables assignation. The last time a and b are used is to compute d. So we could save d value in a or b directly. This would give something like:

a = 10
b = 15
c = a + b
a = a - b
a = c * a 

This kind of optimisation is not required in modern programming langage cause compilers will do it for you, so it is better to keep a clear variable management that will help anyone to get what is going on.

Let’s move on to the main subject.

Variable scopes

The first point to correctly manage memory is to be aware of variable scope. The scope of a variable is the place she is define and the code part it will exist in. We mostly use local variable while coding. A local variable exists only where it is define and until the definer scope is destroyed.

As an example, in this code:

void anotherScope(void) {
  int c, a;
  c = 10;
  a = 4;

  printf("%d", a + c)
}

void scoped(void) {
  int a, b;
  a = 5;
  b = 4;
  
  anotherScope();
  printf("%d", a + b);
}

int main(void) {
  scoped();
  return 0;
}

The variables defined in anotherScope function exists only while it is running. The a variables in scoped and anotherScope are distinct and does not affect each others. This is due to scope isolation. A function call create a scope in witch variables will exists. Only variables defined by the function (entry parameters or locally defines) exists in this scope. If you need to share values between functions with a single name, you have to use global variables. In most langages, variables defined outside of function scope are considered global. Some can require the use of a global key word. If we redefined the previous code with a global a variable:

int a

void anotherScope(void) {
  int c;
  c = 10;
  a = 4;

  printf("%d", a + c)
}

void scoped(void) {
  int b;
  a = 5;
  b = 4;
  
  anotherScope();
  printf("%d", a + b);
}

int main(void) {
  scoped();
  return 0;
}

anotherScope and scoped change a global variable. This change the result. The initial result was

14
9

while it will give

14
8

when using a globally defines a.

Defining a global variables will help saving space when you need to share data between multiple functions around the code but can make it hard to debug issues happening when multiple part of the code modifies it in an unexpected order.

This code:

#include <stdio.h>
#include <stdlib.h>

int a

void anotherScope(void) {
  int c;
  c = 10;
  a = 4;

  printf("%d should be 14", a + c)
}

void scoped(void) {
  int b;
  b = 4;

  printf("%d should be 9", a + b);
}

int main(void) {
  a = 5;
  anotherScope();
  scoped();
  return 0;
}

Expect a comportement witch is not respected. Variable a is set to 5 in main and the scoped function expect it to stay at 5. But anotherScope set a to 4 without restoring the older value so when called before scoped, it affects the result and create a bug.

This comportment can lead to very tricky situation in larger code base where a global variables is used and altered by many functions.

Pointers

Another way to save memory is to use Pointers. A pointer is a direct reference to the memory. Instead of managing a value, we manage the place it is stored. An address is a light hexa object so it is quickly lighter than heavy struct. Using pointers instead of direct value will avoid copying data when manipulating the object in a specific function. As pointers are memory reference, you have to release it when you know it will not be used or you lost the tracking. Most of the modern langages does it by themselves relying on a garbage collector who keep track of pointers and variables and delete them when they are not used. But it can lead to some issue in langages where you have to release it by yourself like C.

Let’s manipulate some C pointers using the swap method implementation.

We have a swap method define like:

#include <stdio.h>
#include <stdlib.h>

void swap(int a, int b) {
  b = a + b; // get sum of A and B
  a = b - a; // A + B - A = B -> A is now B
  b = b - a; // A + B - B = A -> as A is B, and B is A+B, B is now A
  printf("First value is now: %d and second value %d", a, b); // swap done
}

int main(int argc, char *argv[]) {
  int a, b;

  if (argc != 2) { // check that we received 2 arguments
    printf("Swap expect 2 arguments, %d received!", argc)
    return 1 // return 1 cause we received an unexpected number of arguments witch is an error
  }

  a = atoi(argv[0])
  b = atoi(argv[1])

  swap(a, b)

  return 0
}

This code print the swapped values, but the swap method has no real use as it does nothing to provided values, nor does it provided the swapped couple. Let’s use pointers to modify the provided reference so swap will have a real use.

#include <stdio.h>
#include <stdlib.h>

// This method swap a and b values relying on
// a value computed swap
void swapValue(int *a, int *b) {
  *b = *a + *b;
  *a = *b - *a;
  *b = *b - *a;
}

// This method swap the address of a and b
void swapPointers(int **a, int **b) {
  int *c;
  c = *b;
  *b = *a;
  *a = c;
}

int main(int argc, char *argv[]) {
  int a, b;
  int *pa, *pb;

  if (argc != 3) { // check that we received 2 arguments
    printf("Swap expect 2 arguments, %d received!", argc);
    return 1; // return 1 cause we received an unexpected number of arguments witch is an error
  }

  a = atoi(argv[1]);
  b = atoi(argv[2]);
  pa = &a;
  pb = &b;

  swapValue(pa, pb);
  printf("First value is now: %d@%p and second value %d@%p\n", *pa, pa, *pb, pb); // swap done
  swapPointers(&pa, &pb);
  printf("First value is now: %d@%p and second value %d@%p\n", *pa, pa, *pb, pb); // swap done

  return 0;
}

The new swapValue method expect reference to an integer value (int *a indicates that a should be a pointer). It does not modifie the address but directly the value containing by those addresses. The other one, swapPointers, does not modify the values of the pointers but it inverse the pointers to a and pointers to b.

Hope those small example will help you. See you later 🙂

Les bases de la programmations

Hello World! Ici Switch. Aujourd'hui, je vais vous initier a la programmation en me basant sur quelque langages de programmation. Je ferais de mon mieux pour vous expliquer comment écrire de simple programme et transcrire du code d'un langage a un autre.

Dans cet article, j'utiliserai: C, OCaml, Golang, Python et un pseudo-code personnalisé pour résoudre quelques problèmes basique.

Bases de l'organisation de code

Lorsque l'on apprend a coder, il est fréquent de regrouper tous le code au même endroit, résolvant le problème maladroitement. Par exemple, il est fréquent de commencer par écrire :

afficher("Hello World!")
afficher("Hello Switch!")
afficher("*")
afficher("**")
afficher("***")
afficher("****")

Lorsqu'il est demandé d'afficher :
Hello World!
Hello Switch!
*
**
***
****

Et cela fonctionne.

Bien que cet example soit exagéré, il représente assez bien ce que les débutants auront du mal a équilibré: la séparation du code.

Les langages de programmation proposent différentes solutions pour nommer des valeurs ou définir des actions répétées. Nous pouvons simplifier l'example précédent en définissant une fonction pour dire bonjour

fonction Bonjour(à):
  message = "Hello" + à + "!"
  afficher(message)

Afin d'éviter la répétition de l'affichage Hello+nom+!.

Ils apportent également des solutions pour les tâches répétées. La majorité des langages actuel intègrent les boulces for et while ou acceptent les fonctions récursives (une fonction s'appelant elle même). Ainsi, nous pouvons afficher le triange d'* grâce a la boucle

stars = ""
pour i allant de 0 à 4 faire:
    stars = stars + "*"
    afficher(stars)

L'une des première difficulté est de savoir si un ensemble de ligne de code doit être isolé dans une fonction dédié ou si un process est suffisamment répétitif pour être intégrer dans une boucle d'itération.

Il existe de nombreuses architecture pour les projets informatique, chacune ayant ses forces et faiblesses. Dans ce blog, j'utiliserai principalement une séparation modulaire simple (chaque fonctionnalité a son propre module sans règles réelle) et la Clean Architecture pour les bases de code plus complexes (séparation claire entre les objectifs du code, la gestion des données, l'interface avec l'utilisateur et les éléments fonctionnels).

Passons à la résolution de problèmes.

Inversion de variables (C - OCaml)

Commençons par écrire un algorithme permettant d'inverser deux variables. La solution la plus simple utilise une variable complémentaire comme tampon pour conservé la valeur initiale d'une des deux variables, ce qui peut s'exprimer ainsi :

a = 125
b = "trust"
c = b
b = a
a = c

Maintenant, supposons que notre system n'a plus la place nécessaire a l'attribution d'une tierce variable, et proposons un algorithme ne reposant pas sur un tampon.

Pour simplifier le problème, supposons que nos variables soit nécessairement des nombres. Si j'ai une variable A ayant pour valeur 5 et une variable B équivalent à 2, comment puis-je avoir A = 2 et B = 5 sans variable complémentaire. Une solution peut être dérivée des opérations conservatives entre deux valeurs. En les utilisant, je peux combiner les deux valeurs en une et récupérer les valeurs non combinées depuis la combinaison obtenue. Disons que nous sommions A et B dans B. B prend alors la valeur 7 tandis que A contient toujours 2. Lorsque nous retirons A de B dans A, nous obtenons A = B - A = 7 - 2 = 5 qui est la valeurs initiale de B. Et si nous retirons de nouveau A de B, nous récupérerons la valeur initiale de A car A est la valeurs initiale de B et B la somme des valeurs initiales. Cette transformation résulte des propriétés de transitivité et de symétrie de l'addition et de la soustraction. A = A + B - B et B = A + B - A, aussi, en faisant notre suite d'opération, nous réalisons l'inversion en attribuant à A la valeur de B A + B - A et inversement pour B.

a = 7
b = 2
b = a + b
a = b - a
b = b - a

Maintenant, codons.
Notre première implémentation sera en C. C est un des premiers langages haut niveau. Créé en 1972, il reste une référence pour l'informatique embarquée (tableau de bord des voitures par example) et est la base de nombreux langages.

Tout programme implémenté en C doit fournir une fonction main renvoyant un type int. Cette fonction peut ne pas prendre d'arguments (void) ou avoir un coupe de paramètres (int argc, char *argv[]). main représente la fonctionnalité et en est le seul et unique point d'entré. L'entier retourné par la fonction permet de gérer les erreurs. Si 0 est retourné, il n'y a pas eu d'erreur, sinon une erreur a eu lieu.

#include LIB; // include a library to use its functions

TYPE NAME(T1 P1, T2 P2, ...) {} // declare a function named NAME returning type TYPE having parameters P1 of type T1 AND P2 of type T2

TYPE NAME; // declare a variable NAME of type TYPE

return SMTG; // stop function and make it returns SMTG

funcName(P1,P2,...); // Make a function call to funcName with parameters

Tout programme C doit fournir une fonction main renvoyant une valeur de type int. Cette fonction peut soit ne pas prendre d'argument (type void) soit prendre un couple d'argument (int argc, char *argv[]). Elle représente la fonctionnalité et en est le seul point d'entrée. La valeur entière renvoyée par main indique si des erreurs ont eu lieu lors de l'exécution. 0 signifie qu'il n'y a pas eu d'erreur, toute autre valeur permet de documenter les erreurs.

Définissons la fonctionnalité d'inversion swap comme bloc unique dans la fonction main. Nous devons déclarer deux variables numériques ou deux chaines de caractères (ici: défini signifie qu'elles ont un nom et une valeur) puis réaliser l'inversion.

int main(void) {
  int a, b; // declare two integer variables

  a = 2;
  b = 5;

  printf("First value is: %d and second value %d\n", a, b); // show before swap
  b = a + b; // get sum of A and B
  a = b - a; // A + B - A = B -> A is now B
  b = b - a; // A + B - B = A -> as A is B, and B is A+B, B is now A
  printf("First value is now: %d and second value %d\n", a, b); // swap done

  return 0; // return 0 cause all went well :)
}

Nous avons désormais une première version du code de swap. Isolons le code fonctionnel réalisant l'inversion :

void swap(int a, int b) {
  b = a + b; // get sum of A and B
  a = b - a; // A + B - A = B -> A is now B
  b = b - a; // A + B - B = A -> as A is B, and B is A+B, B is now A
  printf("First value is now: %d and second value %d", a, b); // swap done
}

Nous définissons une méthode swap ayant deux arguments entiers qui inverse les valeurs fournies et affiche le résultat à l'utilisateur.

Proposons également à l'utilisateur du programme de saisir ses propres valeurs. Nous avons deux options pour ce faire. Soit nous laissons l'utilisateur entrer les valeurs pendant l'exécution du programme, soit nous lui demandons de les fournir au lancement de ce dernier. La première solution se base sur la libraire standard (proposée nativement par le langage mais non incluse par défaut) stdio qui fournie des méthodes de lecture et écriture vers les entrées standard. La seconde utilise les arguments spécifiques à la function main (couple argc, argv).

Le code ci-dessous s'attend à ce que l'utilisateur fournisse deux arguments au programme. Si c'est le cas, il applique la méthode swap a ces arguments.

int main(int argc, char *argv[]) {
  int a, b;

  if (argc != 2) { // check that we received 2 arguments
    printf("Swap expect 2 arguments, %d received!", argc)
    return 1 // return 1 cause we received an unexpected number of arguments witch is an error
  }

  a = atoi(argv[0])
  b = atoi(argv[1])

Afin de laisser l'utilisateur saisir les valeurs qu'il désire au vol, nous pouvons proposer une méthode qui lui demandera d'entrer un entier et renverra la valeur saisie au programme.

int getInt(void) {
  int a;
  printf("Enter an integer:");
  scanf("%d", &a); // scan user input and store it in a variable. The & is required      here to let scan method modify a values (pointer concept)
  return a;
}

Supposons à présent que je désire fournir cette même fonction dans un autre langage. Puis-je conserver de bloc, que dois-je adapter ou supprimer afin de conserver un code fonctionnel.

Tout d'abord, l'algorithme utilisé peut toujours être conservé. Son implémentation sera à revoir, mais la méthode en elle même reste valable quelque soit le langage. Ensuite, je dois déterminer les éléments fonctionnels externe que le langage me fournit comme les conversions de valeurs ou la récupérations d'entrées utilisateurs. La majorité des langages de programmations intègre des solutions pour les tâches basiques mais certains n'en proposent qu'une partie ou aucune. Je peux alors réécrire mon code pour le nouveau langage choisit. Ré-implémentons notre inversion en OCaml.

OCaml est un langage dérivé de C mais ça syntax est assez différente. C'est également un langage fortement typé mais il utilise un typage dynamique plutôt que statique (il est possible de forcer le type des fonctions via des fichiers spécifique mais si ce n'est pas fait, OCaml déterminera lui-même le type le plus adapté).

Traduisons notre code C en OCaml. Si C requiert une fonction main, ce n'est pas le cas en OCaml. Les fichiers de code sont exécuté tel quel. Nous pouvons donc simplement créer notre fonction et son appel dans un fichier.

let swap a b = 
  let b = a + b in 
  let a = b - a in 
  let b = b - a in 
  (a, b);;

swap 2 5;;

Notre code en OCaml fait exactement la même chose qu'en C, si ce n'est que nous retournons le couple inversé plutôt que de l'afficher. L'utilisation de let ... in permet de redéfinir localement les variables a et b avant de renvoyer le couple inversé (a, b). L'appel swap 2 5 nous fournit le couple 5, 2 en réponse.

Vous trouverez d'autres adaptations du même programme pour d'autres langages directement sur github: https://github.com/switchelven/programming-bases. Je ne les détaillerais pas car sur un code aussi simple, elle sont équivalente a la transition de C vers OCaml. Passons plutôt au problème suivant.

Min and max (Go)

Intéressons nous maintenant a une fonction permettant de récupérer le minimum et le maximum d'une liste. Par example, en l'appliquant à la liste 2,4,1,3,5,7,9, je devrais obtenir 1 pour le minimum et 9 pour le maximum. Encore une fois, il existe de nombreuses solutions pour résoudre le problème. La plus simple compare chaque élément de la liste 1 a 1 au maximum et minimum actuels connus. Une autre trierai les données dans un ordre connue pour récupérer les éléments directement. Il y a également plusieurs façons de comparer les valeurs.

Commençons par la partie comparaison. La majorité des langages fournissent des opérateurs de comparaison directs pour leurs types basiques. Mais voyons comment s'en passer pour le plaisir de l'exercice. Pour être plus précis, voyons comment calculer directement le minimum et le maximum entre deux entiers. En se basant sur l'égalité A+B+A-B=2A et les valeurs absolues, nous devrions pouvoir retrouver notre comparaison. Nous savons que |A-B|=A-B lorsque A > B et que |A-B|=B-A si B > A. Ainsi, nous pouvons en déduire que A+B+|A-B| donnera 2A si A > B et 2B si B > A. Nous pouvons donc proposer l'expression suivante pour calculer le minimum et le maximum:

fonction max(a, b) {
  return (a+b+|a-b|)/2
}

fonction min(a, b) {
  return (a+b-|a-b|)/2
}

Implémentons ces fonctions. Cette fois, je vous propose le code en Go, à vous de le transcrire.


func Min(a int, b int) uint {
   return uint(a+b-abs(a-b)) / 2
}

func Max(a int, b int) uint {
   return uint(a+b+abs(a-b)) / 2
}

func main() {
   fmt.Println("Min of", 135, 209, "is", Min(135, 209))
   fmt.Println("Max of", 178, 179, "is", Max(178, 179))
}

Vous remarquerez que les méthodes Min et Max retournent des entiers non typés (uint) plutôt que typés. J'ai fait ce choix pour indiquer que l'algorithme ne fonctionne pas correctement avec des nombres négatifs. Utiliser des entiers non typés indique que les méthodes ne renverrons jamais d'entier négatif et donc ne les gèrent pas correctement. Si vous testez les méthodes avec un retour d'entier typé, vous constaterez que la méthode fonctionne comme attendu pour des entiers positifs ou de type différent. Mais entre deux entiers négatifs, les réponses entre Min et Max seront inversées car la différence utilisée pour trouver l'ordre est une valeur absolue.

Appliquons maintenant ces méthodes a une liste de valeurs. Je présentais plus haut deux idées pour trouver les valeurs extrêmes d'une liste. Comparer les éléments un a un est une meilleur idée que de trier la liste si la liste en question est aléatoirement triée. Si la liste est assez bien triée, finir de la trier sera beaucoup plus rapide. Comme nous ne pouvons pas déterminer à quel point une liste est triée à l'exécution (sans parcourir l'intégralité de la liste), il nous faut choisir l'algorithme en fonction de préconditions qui peuvent être assurer si la données est interne ou en se basant sur une structure de donnée spécifique. Nous pourrions par example définir une liste qui serait toujours triée en forçant l'ajout d'un nouvel élément a sa place triée.

En supposant que la liste soit aléatoirement triée, l'implémentation la plus simple pour récupérer la plus grande et la plus petite valeur dans une liste d'entier en Go est

func MinMaxList(intList []int) (min uint, max uint) {
    // Set default value if slice is empty
    min = 0
    max = 0

    // Search for min and max in slice
    for i, x := range intList {
        // Set max and min on list fist element. 
        if i == 0 { // i is the index of element. Go starts indexing slices at 0
            max = x
            min = x
            continue
        }

        // Check if current min/max are still min or max comparing to current value
        max = Max(int(max), x)
        min = Min(int(min), x)
    }

    return // return computed min, max. The empty return in go will automatically returns named return parameters
}

Puissance (Python, OCaml)

Parlons puissance. La notion mathématique de puissance permet d'écrire simplement la multiplication répétée d'un nombre (puissance(2,3)=2*2*2=8). Nous connaissons déjà une méthode pour répéter un nombre connu de fois une opération en se basant sur les boucles for. Nous pouvons l'exprimer ainsi en pseudo-code:

function pow(a, b) {
  res = 1 // pow(number, 0) = 1
  for _ from 0 to b do:
    res = a * res
  return res
}

Une autre solution se base sur la notion de récursion. Les fonctions récursives définissent des cas simple dont la résolution est connue (généralement appelé cas simple ou cas de sortie). Pour la fonction puissance, nous retenons généralement un cas simple unique: puissance(n, 0) = 1. Il reste alors a trouver des méthodes dites de simplifications qui permettent d'aller de n'importe quel cas vers les cas de sorties. Dans notre programme, nous utiliserons la construction naturelle de la puissance : puissance(a, n) = a * power(a, n-1). Notre fonction récursive peut donc se décrire ainsi :

function pow(a, n) {
  if n == 0 {
    return 1
  }
  return a * pow(a, n-1)
}

Les fonctions récursives sont un outils puissant mais il faut être vigilant vis à vis de sa complexité spatiale (l'espace mémoire requis pour l'exécuter). Si nous décomposons l'exécution de la fonction power(10,4), nous obtenons le processus suivant :

appeler puissance(10, 4):
puissance(10,4) = 10 * puissance(10,3)
  appeler puissance(10, 3):
  puissance(10,3) = 10 * puissance(10,2)
    appeler puissance(10,2):
    puissance(10,2) = 1O * puissance(10,1)
      appeler puissance(10,1):
      puissance(10,1) = 10 * puissance(10,0)
        appeler puissance(10,0):
        puissance(10,0) = 1
        renvoyer 1
     puissance(10,1)=10 * puissance(10, 0) = 10 * 1 = 10
     renvoyer 10
   puissance(10,2) = 10*puissance(10,1) = 10 * 10 = 100
   renvoyer 100
  puissance(10,3) = 10 * puissance(10,2) = 10 * 100 = 1000
  renvoyer 1000
puissance(10, 4) = 10 * puissance(10,3) = 10 * 1000 = 10000
renvoyer 10000

Nous pouvons constater que chaque appel récursif garde en mémoire une opération a exécuter en attendant que l'état suivant soit calculé. Lorsque l'état suivant a fini sont cycle de vie et a renvoyé sa valeur, l'opération mise de côté sera exécutée avec la valeur obtenue. Une erreur basique lorsque l'on travaille sur des fonctions récursives est de définir des cas de simplification incorrect. Ces étapes ont pour objectif d'exécuter une opération incomplète afin de se rapprocher des cas simples. Si les simplifications n'aident pas a se rapprocher des cas de sorties, ils ne seront jamais atteint et la méthode tournera dans le vide jusqu'à remplir la mémoire conduisant à une erreur. Une autre erreur fréquente consiste a mal construire les résultats. La fonction atteint bien ses cas simples, mais le retour est vide ou incorrecte car la simplification n'associe pas correctement le résultat. Maîtrisées, les fonctions récursives permettent d'exprimer clairement des algorithmes complexes. Nous pouvons désormais implémenter les deux méthodes. Je vous les présente en OCaml pour la méthode récursive et en Python pour la boucle.

def pow(a, n):
    res = 1
    for _ in range(0, n):
        res = res * a
    return res

if __name__ == "__main__":
    pow(3,10)
let rec pow a n = 
  match n with
  | 0 -> 1
  | _ -> a * pow a (n-1);;

pow 3 10;;

Définition du Pseudo Langage

Le pseudo-langage utilisé dans cet article se définit ainsi:

X = Value -> stock Value dans la variable X la rendant accessible via X directement dans le code (ex: etoile = "*" permet de récupérer le caractère * sous le nom etoile)

afficher(QQCHOSE) -> affiche QQCHOSE a l'utilisatieur

+ -> correspond a l'addition sur des nombres, a la concaténation des objects sinon (ex: 1+4=5, "to"+"to"="toto"

Définition du langage OCaml

OCaml a été introduit en 1996 par l'institut français INRIA. Il est pensé pour aider à démontrer des théorèmes mathématiques et à une syntaxe singulière dans le monde de la programmation.

open LIB;; (* include a library to use its functions *)

let NAME  P1 P2 = (* declare a function named NAME having parameters P1 and P2 *)
let NAME = VAL;; (* declare a variable NAME having value VAL *)
let rec NAME P1 P2 = (* declare a recursive function named NAME having parameters P1 and P2 *)

funcName(P1 P2 ...);; (* Make a function call to funcName with parameter *)

match (P1, P2, ...) with (* match P1, P2, ... with values)
| A, B, ... ->           (* P1=1, P2=B, ... then execute *)
| _, C, ... ->           (* P1 is any, P2=C, ... then execute *) 

Définition du langage Go

Go est un langage développé par Google et apparu en 2009. C'est un langage classique avec un typage fort et statique.

import MODULE // import module MODULE

var NAME TYPE // declare a variable named NAME of type TYPE

func NAME(P1 T1, P2 T2) (RT1, RT2) // define a function NAME having parameters P1 of type T1 and P2 of type T2 returning a couple of type RT1, RT2

Définition du langage Python

Python est un langage complet qui cherche la simplicité et l'accessibilité pour l'homme. La première version est sortie en 1980 et c'est un langage faiblement typé voir non typé. Il est très souple et ça syntaxe est permissive, ce qui le fait paraitre comme un bon langage d'apprentissage. Mais c'est plutôt le contraire. Le langage ne propose aucune sécurité contre des erreurs communes telles que les interférence due a des mélanges de type, et c'est une erreur très fréquente lorsque l'on débute. Il est cependant un excellent langage, particulièrement apprécié en data science.

import MODULE // importe le module MODULE
import MODULE from path.to.file // importe un module précise depuis un fichier/module

def FUNC(P1, P2): // définit une fonction FUNC acceptant P1 et P2 comme paramètres

A = VAL // définit une variable A valant VAL

En quoi consiste la programmation

Hello World. Ici Switch. Aujourd'hui, je vous propose de redécouvrir ce qu'est la programmation.

D'aucun pensent qu'implémenter un programme est relativement simple, qu'il suffit d'associer des pĥrases correctement pour tout faire fonctionner. Peux comprennent les concepts derrière le fonctionnement des outils électronique et de la programmation.

Vous saurez peut être qu'un ordinateur n'est rien d'autre qu'un calculateur logique reposant sur des valeurs binaire (0 ou 1). Toutes ces données sont stockées dans des espaces limitées et chaque demande de communication est réalisée en associant et calculant les interactions logique entre les différents groupes.

Histoire

L'informatique dérive des automates et de l'encryption de message. Des code secret basique de l'antiquité à Enigma en passant par des automates musicien, l'automatisation fit évoluer ses bases mathématique et ses technologies progressivement.

Le premier programme informatique est généralement attribué à Ada Lovelace en 1843. Elle proposa un un algorithme permettant de calculer les nombres de Bernouilly et fonctionnait sur la Machine analytique.

Cette publication ouvrit la voie pour la programmation moderne. Les bases introduites par la machine analytique furent retravailler au fil du temps en ajoutant des capacitées de stockage, de contrôles, et autre, permettant la transition entre les calculateurs électronique et informatique.

L'apparition des calculateurs électronique s'accompagna de l'apparition des premiers langages pour le code machine. Ils permirent de représenter basiquement les algorithme de façon compréhensible pour les systèmes informatisés. Cependant, ces languages d'assemblages sont difficilement compréhensible et demande beaucoup de connaissance en informatique pour créer des programme optimisés.

Ainsi arrivèrent les langages dit de haut niveau et le concept de compilation. La compilation est une méthode de traduction entre un langage humainement compréhensible et le code machine ou un langage de plus bas niveau.

La programmation ?

La programmation est une application de théorie mathématique afin de fournir des interaction connue. Bien qu'aujourd'hui, les langages de haut niveau permettent de s'abstraire des mathématiques pure, les théories avancées de programmation telle que l'intelligence artificielle restent dépendantes de concepts mathématiques complexes.

En bref, un programme informatique consiste en une suite d'action nécessaire au calcul d'un résultat, autrement dit, un algorithme. Tout ce qui est fait par les programme lors de l'utilisation d'un ordinateur se base sur des algorithmes.

Le travail d'un développeur est donc de proposer l'algorithme le plus adapté à la demande du clients et à ses contraintes. Nous n'utiliserons pas le même algorithme pour trier une liste en fonction de l'environnement de déploiement ou de conditions particulière connue sur la liste.

En espérant que cet article puisse vous servir :)

Sources

Wikipédia: https://fr.wikipedia.org/wiki/Programmation_informatique (fr)


Documents complémentaires

Ada Lovelace: https://fr.wikipedia.org/wiki/Ada_Lovelace, https://femmessavantes2.pressbooks.com/chapter/ada-lovelace-mathematicienne-1815-1852/
Nombres de Bernouilly: https://fr.wikipedia.org/wiki/Nombre_de_Bernoulli
Machine analytique: https://fr.wikipedia.org/wiki/Machine_analytique

Mon entrée dans le monde de la programmation

Bonjour le monde. Ici Switch.

Aujourd'hui, j'aimerais vous partagé mon parcours dans le monde de la programmation, comment je l'ai découvert, et comment j'ai évolué en son sein jusqu'à présent.

Cette histoire commence dans ma jeunesse (pour certains, déjà trop tard).

Premiers pas

Je pense que lorsque j'étais au collège, il y avait deux choses qui pouvait nous amener a découvrir la programmation. Soit vous vouliez créer un jeux vidéo, soit vous désiriez partager votre vie, projet ou autre grâce a votre propre site web.

Pour ma part, j'ai créé mes premier site web vers la fin de mes années de collèges. Rien de bien incroyable, j'utilisai alors un éditeur graphique spécialisé gratuit: Nvu (qui deviendra Kompozer).

Erreur après erreur, j'ai pu mettre en place des mises en forme, des interaction trouvées sur d'autre site ou motivé par la volonté de présenté quelque chose de façon particulière. Petit à petit, j'ai décomposé les solutions que je trouvais, comprenant au fur et a mesure les liens entre les différents éléments, leur objectifs ou leur fonctionnements. Je suis petit a petit passé d'une création de page via un éditeur aidant a la mise en forme à la création directe de code source. S'ensuivit une modification de mes outils vers des éditeurs de code plus classique (Coda, qui fut gratuit sur MacOS puis Netscape et Sublime Text).

Bien que simple, et que beaucoup disent avec humour que codé avec HTML et CSS n'est pas programmé, ce travail m'a donnée des bases solides pour mon évolutions future. J'ai principalement appris comment réutilisé des ressources existantes pour les adapté a un projet différent. Mais aussi, comment construire un projet complet, pièce par pièce, du néant à sa publication en ligne. Je dirai aujoud'hui, avec le peu de recul dont je dispose, que comprendre comment créer un outils a partir d'une simple idée est essentiel a la compréhension des outils plus avancé proposant de simplifié la mise en place de tel projets.

Découverte des algorithmes

La prochaine étapes de ma découverte a été amené par mon entré au lycée. Une nouvelle dimension de la programmation s'est ouvert a moi grâce a la découverte des calculatrice programmable. En France, il est demandé d'acquérir des calculatrice intelligente lors de votre entrée au lycée. Celles-ci permettent par example de créer des graphiques a partir de function, de les analyser ou de résoudre des équations. Mais surtout, elles offrait un langage de programmation, et la possibilité de créer des programme de calcul automatisés.

Le langage proposé, quoi que simpliste, utilisait toutes les définitions basiques de n'importe quel langage impératif.

  • boucle simple
  • déclaration de variable
  • assertions

J'ai ainsi pu implémenter les algorithmes mathématiques apportés par mes études, tel que la division euclidienne, la résolution d'équation du second degrés, et autres. Cette période m'a permis de me séparer d'un grand nombre d'erreur de débutant et en souhaitant complexifier mes programmes, j'ai aussi rencontré la limite de cet outil. Il était impossible de réutiliser des fonctions déjà existante au sein d'un nouvel algorithme. J'ai donc du voir mes programme comme des blocs individuels pouvant être chainés, et ainsi, du clarifier et limiter les entrées et sorties de chaque fonction. Je me doit aussi de préciser que le langage offert par ma calculatrice n'était pas typé (c'est à dire qu'il n'était pas possible lors de la création de la fonction de forcer l'utilisateur a passer les arguments attendu). Cela m'a conduit a vérifier les entrées utilisateur directement dans mon algorithmes. Il existe, a ma connaissance, deux visions de la sécurisation des entrées. La première se contente de déléguer à l'utilisateur de la fonction la tâche de s'assurer que les paramètres correspondent a ce qui est attendu. Cela implique d'assurer que chaque personne utilisant la fonction connaisse les pré-requis du process. Ainsi, si un appel incorrect est fait, l'utilisateur est responsable. L'autre solution consiste a vérifier l'entré soit même et a s'assurer que les pré-requis sont validés. Je préfère cette méthodes ayant appris la programmation via un langage non typé et ne permettant pas de documentation. Aussi, si la première méthode convient a des usages internes, elle ne saurait être appliqué si les données sont fourni par un utilisateur externe. Ainsi, choisir de vérifier les paramètres d'entrée par défaut permet d'assurer que la méthode peut aussi bien être utilisé en interne qu'en externe sans créer une couche de sécurité.

Apprentissage scolaire

Mon parcours scolaire me conduit par la suite en classe préparatoire mathématique et physique (MPSI puis MP) et me fournit ma première approche scolaire de la programmation.

Ces deux années m'ont permis de découvrir un de mes paradigme de programmation préférée, la programmation fonctionnelle. Cette rencontre a clarifié et élargie ma vision sur la conception de programme informatique. J'ai appris un grand nombre de méthodes pour simplifié des problèmes, les séparer en sous-problème ou les liés a des théories mathématiques. C'est ce qui m'a conduit a choisir la voie des sciences informatique pour la suite de mon parcours.

Pour finir, mon passage en école d'ingénieur et mes premières expériences professionnelles m'ont discipliné et m'ont encouragé a utiliser des outils existant plutôt que de toujours tout recréer soit même.

Aujourd'hui, j'ai parcouru de nombreuses facette des sciences informatiques et, bien que je me soit orienté sur le développement Back-End (ce que vous ne voyez pas mais qui permet aux applications de fonctionner correctement), je continue a approfondir mes connaissances dans d'autres sujet. J'adore le devops et l'intelligence artificielle, et je ne manquerais pas de vous partager mes découvertes.

J'espère que cet article vous aura été une agréable lecture, et vous souhaite une très bonne année 🙂

fr_FR