Skip to content

klaufir/libCello

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

libCello

Example

/*
** Example libCello Program
*/

#include "Cello.h"

int main(int argc, char** argv) {

  /* Stack objects are created using "$" */
  var int_item = $(Int, 5);
  var float_item = $(Real, 2.4);
  var string_item = $(String, "Hello");

  /* Heap objects are created using "new" */
  var items = new(List, 3, int_item, float_item, string_item);
	
  /* Collections can be looped over */
  foreach(item in items) {
    /* Types are also objects */
    var type = type_of(item);
    print("Type: '%$'\n", type);
  }
  
  /* Heap objects destroyed with "delete" */
  delete(items);
  
  /* Tables require "Eq" and "Hash" on key type */
  var prices = new(Table, String, Int);
  put(prices, $(String, "Apple"),  $(Int, 12)); 
  put(prices, $(String, "Banana"), $(Int,  6)); 
  put(prices, $(String, "Pear"),   $(Int, 55)); 
  
  var pear_price = get(prices, $(String, "Pear"));
  print("Price of a 'Pear' is %$\n", pear_price);

  /* Tables also supports iteration */
  foreach(key in prices) {
    var price = get(prices, key);
    print("Price of %$ is %$\n", key, price);
  }
  
  /* "with" automatically closes file at end of scope. */
  with(file in open($(File, NULL), "prices.bin", "wb")) {
  
    /* First class function object */
    lambda(write_pair, args) {
      
      /* Run time type-checking with "cast" */
      var key = cast(at(args, 0), String);
      var val = cast(get(prices, key), Int);
      
      /* Format specifier for objects is %$ */
      print_to(file, 0, "%$ :: %$\n", key, val);
      
      return None;
    };
    
    /* Higher order functions */
    map(prices, write_pair);
  
  }
  
  delete(prices);
  
}

About

libCello is a GNU99 C library which brings higher level programming to C. It takes inspiration from Obj-C, Haskell and Python. Most closely libCello resembles a C dialect with interfaces, dynamic/duck typing, exceptions, and some syntactic sugar. There are a selection of new keywords, and many generically named functions in the namespace, but other than that it should be fully compatible with normal C code.

Although I've made the syntax pleasant, libCello isn't a library for beginners. It is for C power users, as manual memory management doesn't play nicely with many higher-order concepts.

The high level stucture of libCello projects is inspired by Haskell and C, while the syntax and semantics are inspired by Python and Obj-C. Object Orientation in C was not the goal of this project, but I hope that with libCello I've turned C into something of a dynamic and powerful functional language which it may have once been.

libCello was created just for fun as an experiment to see how far we could push the C language. Therefore libCello should NOT be used for production code! Many of the data structures are still inefficient in their implementation and using this library sacrifices much of the compile-time safety of the C language. Please understand the risks before you choose to use libCello.

More Examples

Some functional tools using lambda statements.

#include "Cello.h"

int main(int argc, char** argv) {
  
  /*
  ** Basic method of lambda construction .
  ** Must be at the statement level.
  ** Cannot be used in/as expression.
  */
  lambda(hello_name, args) {
  
    /* Input is a list of arguments */
    var name = cast(at(args, 0), String);
    print("Hello %s!\n", name);
    
    /* Always must return */
    return None; 
  }
  
  /* Functions called with "call" */
  call(hello_name, $(String, "Bob"));
  
  /* Higher order functions supported */
  var names = new(List, 3, $(String, "Dan"), $(String, "Robert"), $(String, "Chris"));
  map(names, hello_name);
  delete(names);
  
  /* Here is an example with two arguments */
  lambda(add_print, args) {
    int fst = as_long(cast(at(args, 0), Int));
    int snd = as_long(cast(at(args, 1), Int));
    printf("%i + %i = %i\n", fst, snd, fst+snd);
    return None;
  }
  
  /*
  ** Notice arguments to "call" in curried form.
  ** Use "call_with" for uncurried calling.
  */
  call(add_print, $(Int, 10), $(Int, 21));
  
  /* Partially applied Function, neat! */
  lambda_partial(add_partial, add_print, $(Int, 5));
  
  call(add_partial, $(Int, 1));
  
  /*
  ** We can use normal c-functions too.
  ** If they have all argument types as "var".
  ** Then they can be uncurried.
  */
  var Welcome_Pair(var fst, var snd) {
    print("Hello %s and %s!\n", fst, snd);
    return None;
  }
  
  lambda_uncurry(welcome_uncurried, Welcome_Pair, 2);
  
  call(welcome_uncurried, $(String, "John"), $(String, "David"));
  
  return 0;
}

More More Examples

Defining a new Class.

A Class is a TypeClass, also known as an Interface. These are defined to allow overloaded or generic functions.

They are defined as follows.

In the header...

/** Vector - vector operations */

class {
  float (*dot)(var, var);
  float (*length)(var);
} Vector;

float dot(var self, var obj);
float length(var self);

And in the source file...

float dot(var self, var obj) {
  Vector* ivector = type_class(type_of(self), Vector);
  assert(ivector->dot);
  return ivector->dot(self, obj);
}

float length(var self) {
  Vector* ivector = type_class(type_of(self), Vector);
  assert(ivector->length);
  return ivector->length(self);
}

Defining a new Type.

A Type is an structure which implemenents a number of Classes.

They typically have an associated data object. They can be defined as follows.

In the header...

global var Vec2;

data {
  var type;
  float x, y;
} Vec2Data;

/** Vec2_New(var self, float x, float y); */
var Vec2_New(var self, va_list* args);
var Vec2_Delete(var self);
void Vec2_Assign(var self, var obj);
var Vec2_Copy(var self);

var Vec2_Eq(var self, var obj);

float Vec2_Dot(var self, var obj);
float Vec2_Length(var self);

instance(Vec2, New) = { sizeof(Vec2Data), Vec2_New, Vec2_Delete };
instance(Vec2, Assign) = { Vec2_Assign };
instance(Vec2, Copy) = { Vec2_Copy };
instance(Vec2, Eq) = { Vec2_Eq };
instance(Vec2, Vector) = { Vec2_Dot, Vec2_Length };

And in the source file...

var Vec2 = methods {
  methods_begin(Vec2),
  method(Vec2, New),
  method(Vec2, Assign),
  method(Vec2, Copy),
  method(Vec2, Eq),
  method(Vec2, Vector),
  methods_end(Vec2)
};

var Vec2_New(var self, va_list* args) {
  Vec2Data* v = cast(self, Vec2);
  v->x = va_arg(*args, double);
  v->y = va_arg(*args, double);
  return self;
}

var Vec2_Delete(var self) {
  return self;
}

void Vec2_Assign(var self, var obj) {
  Vec2Data* v1 = cast(self, Vec2);
  Vec2Data* v2 = cast(obj, Vec2);
  v1->x = v2->x;
  v1->y = v2->y;
}

var Vec2_Copy(var self) {
  Vec2Data* v = cast(self, Vec2);
  return new(Vec2, v->x, v->y);
}

var Vec2_Eq(var self, var obj) {
  Vec2Data* v1 = cast(self, Vec2);
  Vec2Data* v2 = cast(obj, Vec2);
  return (var)(v1->x is v2->x and v1->y is v2->y);
}

float Vec2_Dot(var self, var obj) {
  Vec2Data* v1 = cast(self, Vec2);
  Vec2Data* v2 = cast(obj, Vec2);
  return (v1->x * v2->x + v2->y * v2->y);
}

float Vec2_Length(var self) {
  Vec2Data* v = cast(self, Vec2);
  return sqrt(v->x * v->x + v->y * v->y);
}

More More More Examples

Memory management is hard. Very hard when combined with a lack of rich stack types. Very very hard when combined with a whole load of high level concepts. libCello gives you a few options, which where possible, the standard library is agnostic too. You can use what you think is best.

  • Destructive Operations - Most of the standard library uses destructive operations and expects the user to make a copy if they exlicity want one.
  • Output Parameters - In some places it is more appropriate to use output parameters and in which case assign is used to move the data around.
  • Reference Objects - References wrap standard objects, where with can be used to declare their lifetime. And at can be used to dereference.
local void object_lifetime_example(void) {
  
  with(liferef in $(Reference, new(String, "Life is long"))) {
    print("This string is alive: %$\n", at(liferef,0));
  }

  print("Now it has been cleared up!\n");
  
}

/* They can also be stacked */

local void many_object_lifetimes(void) {
  
  with(liferef0 in $(Reference, new(String, "Life is long")))
  with(liferef1 in $(Reference, new(String, "Life is Beautiful")))
  with(liferef2 in $(Reference, new(String, "Life is Grand"))) {
    print("%s :: %s :: %s\n", at(liferef0,0), at(liferef1,0), at(liferef2,0));
  }

}
  • Reference Pools - Reference pools are also avaliable which use retain and release to providing a reference counting mechanism.
#include "Cello.h"

local var g_pool;

local void table_fill(var x) {
  put(x, $(String, "First"),  $(Real, 0.0));
  put(x, $(String, "Second"), $(Real, 0.1));
  put(x, $(String, "Third"),  $(Real, 5.7));
  release(g_pool, x);
}

local void table_process(var x) {
  put(x, $(String, "First"), $(Real, -0.65));
  release(g_pool, x);
}

int main(int argc, char** argv) {
  
  g_pool = new(Pool);
  
  var x = retain(g_pool, new(Table, String, Real));
  
  table_fill(retain(g_pool, x));
  table_process(retain(g_pool, x));
  
  release(g_pool, x);
  
  delete(g_pool);
  
}

While useless in such a trivial example, because the pool always cleans up references on delete, it can be very useful to avoid memory leaks in things such as event loops or with general pooled memory. It can be used as a poor mans sweep garbage collector.

More More More More Examples

libCello provides a kind of exception handling to deal with errors.

local var DivideByZeroError = Singleton(DivideByZeroError);

local int try_divide(int x, int y) {
  if (y == 0) {
    throw(DivideByZeroError, "Division By Zero (%i / %i)", x, y);
  } else {
    return x / y;
  }
}

int main(int argc, char** argv) {

  try {
    int result = try_divide(2, 0);
  } catch (e in DivideByZeroError) {
    // Panic!
    return 1;
  }
  
  return 0;
}

One can also catch multiple objects and then write conditional code based on each. Or one can catch all exceptions or any thown object by leaving the specifer list empty.

try {
  do_some_work();
} catch (e in TypeError, ClassError) {
  if (e is TypeError) { print("Got TypeError!\n"); }
  if (e is ClassError) { print("Got ClassError!\n"); }
}

try {
  do_some_other_word();
} catch (e) {
  print("Got Exception: %$\n", e);
}

Throwing an exception will jump the program control to the innermost catch block. If it is not handled here it is passed on to an outer block. To catch an exception one must put a reference to the thrown object. Any object can be thrown and caught as an Exception in libCello so users can create their own Exception types or find other applications for the semantics. The thrown message will be preserved internally, but be careful of throwing stack memory which may become invalidated when jumping to the new location.

More More More More More Examples

There are some more examples in the demos folder as well as a large amount of reference material under tests and via the source code. Native class definitions (such as for New, Eq, Ord) can be found in Prelude.h

Explanation

The first thing that probably comes into your head viewing the above code is var. This is a typedef of void* and is used via convention in libCello code to allow for overloaded functions. As you can see in the example code type checking/hinting can be done at runtime via the cast function.

This allows for a form of poor-man's duck-typing. If an object looks (or sounds) like it has a length, then you are more than free to use len upon it. One can test if a type implements a certain class with the function type_implements(type, Class). Calling a function on an object which does not implement the appropriate classes will throw a ClassError.

Another thing that may have jumped to your mind in the examples is new, delete and the $ symbol. These are ways to allocate memory for objects. new and delete are used for heap objects and call constructors/destructors. $ (which I like to think of as a clef) is used to allocate objects on the stack. It doesn't call a constructor or destructor, but will initialize the corresponding types data structure with the arguments provided. This makes it useful for basic or boxed types.

The idea of with is stolen from Python and will execute enter_with and exit_with on an object on entrance and exit to the block. The behaviour of these can be defined by implementing the With class. Currently it does not handle exceptions inside the block.

Other than these things there is not much surprising in the code which cannot be explained via syntactic sugar.

  • is, isnt, not, and, or -> ==, !=, !, &&, ||
  • elif -> else if
  • local -> static
  • global -> extern
  • class -> typedef struct
  • data -> typedef struct

finally the instance and methods macros are helpers for defining Type objects statically. Because Type objects are somewhat complicated the syntax is very awkward otherwise.

Questions & Contributions

Questions, Contributions and Feedback are more than welcome.

You can e-mail me at:

[email protected]

Or get in contact via the IRC channel at #libCello on Freenode

About

Higher level programming in C

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C 97.7%
  • C++ 2.2%
  • Shell 0.1%