Introduction

Ela is an impure functional programming language with dynamic typing and syntax that is heavily inspired by MLs/Haskell. Dynamic stands for a type system that uses a "don't do it until the last moment" type inference algorithm. In other words, type information in Ela comes available at run time, which has its own drawbacks, but gives type system a lot of flexibility. Functional means that all operations in Ela can be seen in the light of combinations of functions. You can surely write imperative code in Ela - like you can in most functional languages including Haskell - however, in most cases, Ela is not the right language for that. And impure means that Ela allows side effects; you can create a mutable data structure and a mutable variable in Ela which might be really helpful occasionally; however, Ela doesn't favor such style of programming - it fully supports and motivates a programmer to write programs in a pure functional way.

Ela shares a lot of peculiarities of functional programming languages such as - everything being as expression, first class and higher order functions, immutable variables and data structures, pattern matching, algebraic types, and so on. Currently, Ela comes with an Ela Console utility that can be used to execute and compile Ela code, and fully supports interactive mode as well.

In this article, I will give a short overview of Ela implementation and some core language features.

Implementation

Parser

Ela is fully written in C#. Its current implementation consists of four loosely coupled major components: a parser, a compiler, a linker, and a virtual machine.

The parser is written using CoCo/R that can generate LL(1) parsers. CoCo/R does offer some additional tools to resolve conflicts that don't fall under LL(1) strategy; however, these tools are pretty limited. Luckily, Ela grammar is pretty straightforward. The language mostly lacks any context depending constructs (of which there are many in the language in which Ela itself is written), and as a result, Ela grammar fits pretty well into the LL(1) requirements.

In case you haven't heard about parser generators before, these are tools that can turn a pretty complicated task such as a manual parser implementation into a relatively simple one. The idea behind them is as follows - you provide a language grammar as an input and get a generated parser as an output.

A grammar consists of tokens (such as identifiers, integers, strings, etc.) and productions that describe a particular language construct.

For example, this is how a well known conditional operator looks like in Ela:

IfExpr = 
    "if" Expr "then" Expr "else" Expr.

LL(1) describes a specific type of a parser that can be created by CoCo/R. This one works from left to right, and can look ahead for a single symbol. It does introduce certain limitations; however, as I mentioned before, Ela grammar is not too complex and therefore Ela is totally OK with just LL(1) parser. At the same time, parsers generated by CoCo/R are lightning fast and the whole tool is comparably easy to use which renders CoCo/R as a good candidate if you need to generate a parser in C#.

Except for just formal language grammar, a parser definition also contains so called semantic actions - these are places where you create an object model that represents your code (an abstract syntax tree, AST). Ela uses its own language specific AST that reflects some of the language peculiarities such as everything including the whole program being an expression.

Unlike some language implementations (e.g., Ruby 1.8) that interpret code by walking the AST, Ela goes in a slightly different way and generates an intermediate byte code based on the object model that comes from the parser.

That is something that Ela compiler is responsible for.

Compiler

Ela compiler uses a recursive descend to process the AST and generate the appropriate byte code instructions. Besides that, it also generates debug information that is used in such tasks as generation of exception stack traces.

Basically, the compiler is responsible for plenty of things - it calculates the size of the evaluation stack, the size of the "heap" used to store non-temporary objects. Also, Ela supports lexical scope like C or Haskell, and that is what the Ela compiler does as well - it tracks the declarations of variables, allows variables from enclosing scopes to shadow each other, and so on.

Ela byte code (called EIL) contains approximately one hundred instructions and is somewhat inspired by MSIL. However, unlike MSIL which is more or less an object-oriented assembly language, EIL doesn't provide any specific support for OOP, but favors functional paradigm instead, and providers a set of high-level instructions that can be used to create closures, treat functions as first class values, etc.

EIL is a stack based assembly language. For example, a simple expression like (2 + 2) * 3 will be compiled into the following EIL:

PushI4 2
PushI4 2
Add
PushI4 3
Mul

I hope it's clear what actually happens here. The compiler processes an expression from left to right and pushes specific values on the stack. The Add (addition) and Mul (multiplication) operations pop two values from the stack each, perform a calculation, and pushes the result back to the stack. Once the Add instruction is executed, we have just a single value of 4 on the stack; the same after the Mul instruction which pushes on the stack the result of the whole expression evaluation.

All EIL instructions have a fixed stack behavior (e.g., what changes on the stack this instruction performs - like Add that pops two values and pushes a single one back to the stack). Also, EIL instructions have a strict size in bytes. That is basically the reason why byte code is called so. In EIL, a length of an instruction can be either one byte (this is true for all instructions that lack arguments, like Add, Mul, Pop, etc., and this byte is reserved for the instruction code itself) or five bytes (this is true for instructions with arguments, like PushI4 that pushes an integer value on the stack and has a four byte integer as its argument).

You are probably wondering how Ela can deal with data types that don't fit into four bytes - and does it even have any? Let's see what code is generated when you try to compile a 64-bit integer literal 0x7fffffffffffffffL:

PushI4 -1
PushI4 2147483647
NewI8

As you can see, a 64-bit integer value is pushed on the stack in two turns, and then gets assembled using a built-in NewI8 instruction. A more complicated example - creation of a record in the form {x = 1, y = 2} in Ela (records can somewhat resemble anonymous types in C#):

Newrec 2
PushI4_0 
PushI4 1
Pushstr "x"
Reccons 
PushI4_0 
PushI4 2
Pushstr "y"
Reccons

As you can see, this is a whole list of instructions - the first one creates an empty record with reserved slots for two fields, and the rest initialize a particular field by pushing its index, value, and name onto the stack. Reccons is a built-in instruction that is used for record construction.

OK, we have generated byte code. But now we need to interpret it - and that is what a virtual machine is responsible for.

Virtual machine

Ela has a stack based virtual machine (which is obvious as you definitely can't use a Registry based one to interpret stack oriented byte code). Ela VM is a deterministic finite state machine. It is thread safe - Ela functions can be executed in different physical threads and this is a fully legal scenario. Also, unlike some other stack based VM implementations, Ela machine uses two stacks - an evaluation stack where all calculations are performed, and a call stack that stores structures that describe a currently executing function.

Except for several high level instructions that include function calls, built-in data type creation, etc., most of Ela byte code commands are pretty atomic, and as a result, Ela machine surprisingly appears to be a simpler component than a compiler.

Let's see how it works. For example, we need a simple VM that can only operate with integers and supports two operations - addition and multiplication. This is how it might look like:

public int Eval(Op[] ops, int[] args)
{
    var offset = 0;
    var stack = new Stack<Int32>();

    for (;;)
    {
        var op = ops[offset];
        var arg = args[offset];
        
        switch (op)
        {
            case Op.PushI4:
                stack.Push(arg);
                break;
            case Op.Add:
                {
                    var right = stack.Pop();
                    var left = stack.Pop();
                    stack.Push(left + right);
                }
                break;
            case Op.Mul:
                {
                    var right = stack.Pop();
                    var left = stack.Pop();
                    stack.Push(left * right);
                }   
                break;
            case Op.Stop:
                return stack.Pop();
                break;
        }
    }
}

That is really it. As you can see, we have a function that accepts an array of byte code instructions presented through an enumeration for convenience and an array of instruction arguments. For the simplification of instruction decoding, these arrays are always of the same length. If an instruction doesn't have an argument, then there is still a slot for it into the arguments array that contains zero. We also use just a single evaluation stack in this example (Ela has its own stack implementation for the sake of performance, but here we are using a standard class from System.Collections.Generic). The Stop instruction is the one that should always be the very last in our program - this instruction terminates an execution and returns a value that is currently on the top of the stack (we assume that a correct program should always have one). Thanks to the Stop instruction, the core part of the function Eval can be implemented as an endless cycle with no conditions to be evaluated on each iteration, which also boosts performance a little bit.

And that is really it. Simple, isn't it?

Linker

Linker is the last component that I will describe here. It is less complex than the previous components, and its sole purpose is to assemble separate Ela modules into a solid assembly that is actually executed.

Each module in Ela can reference other modules using the open instruction, like so:

open Math

let res = Math.sum 2 2

The linker task is to locate all referenced modules, build them, and to finally combine everything into a single code assembly ready for execution.

Ela linker uses two other components - a parser and a compiler - to build Ela modules. However, it also supports deserialization of Ela byte code from binary (or so called object) files. If it sees that a target directly contains an object file, it will try to read it instead of parsing and compiling the raw code. In many cases, this can dramatically increase the build performance.

Embedding Ela

You can use the Ela interpreter within your .NET application. For such purposes, you only need to reference a single small library - Ela.dll.

You can use the parser and compiler directly from your code as standalone components, but normally you would only need a linker and an Ela machine to execute Ela code.

Ela provides two implementations of a linker. The one that is named ElaIncrementalLinker is used to support interactive mode. It allows you to build and execute Ela code chunk by chunk. The incremental linker is also useful if you want to evaluate Ela code in a string representation. If this is not what you need, you can use a regular Ela linker. This is a sample in C# that shows how to execute Ela code represented as a string:

var l = new ElaIncrementalLinker(new LinkerOptions(), CompilerOptions.Default);
l.SetSource(elaSourceCode);
var res = l.Build();

if (res.Success)
{
    var vm = new ElaMachine(res.Assembly);
    vm.Run();
}

In many cases, you might need to provide some arguments to Ela code. Here is a full example of an Eval function in C# that uses an anonymous class to capture arguments and their names:

public object Eval(string source, object args)
{
    var l = new ElaIncrementalLinker(new LinkerOptions(), CompilerOptions.Default);
    l.SetSource(source);
    var res = l.Build();

    if (res.Success)
    {
        foreach (var pi in args.GetType().GetProperties())
            res.Assembly.AddArgument(pi.Name, pi.GetValue(args, null));
    
        var vm = new ElaMachine(res.Assembly);
        return vm.Run().ReturnValue.AsObject();
    }
    else
    {
        var sb = new StringBuilder();
        
        foreach (var m in res.Messages)
            sb.AppendLine(m.ToString());
    
        throw new ElaTranslationException(sb.ToString());    
    }
}

//sample usage
var r = Eval("$x + $y", new { x = 2, y = 4 });

Notice the $ prefix - it is mandatory if you want to reference an argument from Ela code.

You can also create Ela modules in C# (or any other .NET language). This is an example of a simple module:

public class MathModule : Ela.Linking.ForeignModule
{
    private sealed class RandomizeFunction : ElaFunction
    {
        //Here we need to specify how many arguments our function has
        internal RandomizeFunction() : base(2) { }
            
        public override RuntimeValue Call(params RuntimeValue[] args)
        {
            var rnd = new Random(args[0].AsInt32());
            var ret = rnd.Next(arg[1].AsInt32());
            return new RuntimeValue(ret);
        }
    }
  
    public override void Initialize()
    {  
        base.Add("rnd", new RandomizeFunction(this));  
    }
}

And this is what you need to do to make this module available in Ela:

var l = new ElaIncrementalLinker(CompilerOptions.Default, new LinkerOptions());
l.SetSource(source);
var res = l.Build();
res.Assembly.AddModule("Math", new MathModule());

And now you can seamlessly use this module from Ela:

open Math

let r = Math.rnd 0 42

You can also compile your module into a regular .NET assembly, reference Ela.dll, and specify the following attribute:

[assembly:ElaModule("Math", typeof(MathModule)]

Now you don't have to manually add this module into the collection of modules. The Ela linker will be able to find it without your help. But you will have to specify the DLL name in your open directive like so: open Math[MyMathDll].

Distinctive features

Syntax

As I have mentioned before, the Ela syntax is heavily inspired by the ML family of languages and Haskell. Ela doesn't support layout based syntax but has a freeform syntax like such languages as OCaml or C#. However, Ela doesn't use semicolons to separate statements (a ; operator is used in Ela but has a different meaning).

The top level in Ela can contain either variable declarations or module include directives. For example, the following code is incorrect:

let x = 2
let y = 2
x + y //syntax error

The correct one will be as follows:

let x = 2
let y = 2
let z = x + y

Alternatively, the whole Ela program can be a single expression (e.g., 5, "Hello, world!", 2 + 2 are valid Ela programs). This is pretty useful when you type code directly in the Ela interactive console. In other words, you have two options - either a single expression or a set of declarations.

As you can see, variables are declared using the let keyword. The same for functions:

let sum x y = x + y

let doubleMe x = x + x

Ela fully supports pattern matching which can be described as an old fashioned switch/case construct on steroids - with an ability to match against any value and to compare and bind variables within the single expression:

let array = [| 1,2,3,4 |]
let x = match arr with
        [| 1,2,x,y |] = x + y;
        _             = 0
        end

As you can see, a semicolon operator is used to separate match entries - and that is basically the only meaning that semicolon operator has in Ela.

Besides regular match expression, Ela also supports function definition by pattern matching - pretty similar to the one used in Haskell. Here is an example of a well known map function implemented in Ela:

let map f x::xs = f x :: map f xs;
        _ []    = []

You can introduce both global and local variables using the let keyword. There is also another option to introduce local variables - the where keyword. Here is a tail recursive implementation of Fibonacci function in Ela:

let fib = fib' 0 1
          where fib' a b 0 = a;
                     a b n = fib' b (a + b) (n - 1)
          end

The where binding is not always equivalent to let. For example, it allows you to declare variables that are scoped to a specific entry in the pattern matching construct:

let filter f x::xs | r    = x :: filter f xs;
                   | else = filter f xs
                   where r = f x end;
           _ []           = []

You can also declare several variables at once using the and keyword (this is available for both let and where bindings):

let x = y + 2 and
    y = 2

If the values of these variables are functions, then the declarations are mutually recursive, like so:

let take x::xs = x :: skip xs; 
         []    = [] 
and skip _::xs = take xs; 
         []    = []

Of course, it is difficult to describe all the peculiarities of Ela syntax in this section, especially if you don't know any ML style language. To those who want to know more, I can recommend to read the first article in the Ela overview series that deals with the syntax in more detail.

Curried functions

All functions in Ela can accept only a single argument. Don't panic - this is not a weird Ela limitation, but a pretty typical behavior of functional languages.

A function application operation in Ela is a simple juxtaposition of a function and its argument. The function is always applied to just a single argument. Also, the function application has one of the highest priorities in the language and is left associative. Therefore, when you see code like sum 2 3, this is:

  1. a valid Ela code, and
  2. not a function call with two arguments but two function calls.

Here, we call a sum function with a single argument 2, assume that this function call returns another function, and call it once more with an argument 3.

It actually unveils the way how Ela deals with multiple argument functions. These are basically functions that return other functions (that also might return other functions, and so on). For example, this is how our sum function can be defined using the anonymous function syntax:

let sum = \x -> \y -> x + y

That is equivalent to the following code in C#:

Func<Int32,<Func<Int32,Int32>> sum(int x)
{
    return x => y => x + y;
}

Of course, it is not always convenient to define functions in such a manner - that is why Ela supports special syntax sugar:

let sum x y = x + y //This is fully equivalent to \x -> \y -> x + y

Functions as the sum function above are called curried functions. You probably have heard about this concept before. Curried functions give you a possibility to partially apply functions. As all our functions are just nested closures, you don't have to specify all of the arguments when you call them. A call like sum 2 is fully legal, and returns a new function for one argument that can sum this argument to the number 2.

This feature is usually called partial application, and it is very essential to the functional programming paradigm.

Infix, prefix, postfix

Those three are notations that are used for a function call. Most languages use prefix notation when a function is placed before its arguments. With operators, you usually use infix notation when an operator is placed between its arguments. Postfix notation is less common - it assumes that a function is placed after its arguments.

Prefix notation is widely used in Ela and is the default one. However, sometimes it is more visual to use infix form - that is to place a function name between its arguments. And here is the trick - in Ela, even regular functions can be called using infix form.

There are several cases when this possibility can be useful. Let's say that we have an elem function. This function accepts an element, a list, and tests if a given element exists in a given list. This is how this function can be defined:

let any f x::xs | f x  = true;                 
                | else = any f xs;         
            f []           = false              

let elem x = Samples.any (==x)

OK, but what is so specific about this elem function? Mostly nothing. With the only exception that the application of elem is probably easier to read when it is written in infix form:

elem 5 [1..100]
42 `elem` [1..100]

However, functions are not the only entities that can escape from prefix to infix form. Operators that are applied using infix form, by default, can also be called in the same manner as functions:

let res1 = 2 + 2

let res2 = (+) 2 2

And finally - you can also apply both functions and operators using postfix form. Postfix means that an argument is placed before the function or operator name. This is how it might look:

let isEven x = x % 2 == 0
let res1 = (12 `isEven`)
let res2 = (13 `isEven`)

The same for operators:

let f = (2+) //this is a partial application of + operator
let sum2 = (2+)
let res = sum2 3 //the result is 5

The support for postfix form is really important when it comes to operators as it unveils a very convenient way to partially apply operators. If you partially apply an operator using postfix form, then the very first argument gets "fixed". If you use a prefix form, then the second argument is "fixed":

let div1 = (5-)
let res1 = div1 3 //the result is 2
let div2 = (-5) //this is equivalent to: (-) 5
let res2 = div2 7 //the result is 2

All these tricks work with regular functions as well; however, they are a little bit more common with operators.

As a result, with all this variety of application forms and ability to switch between them, we finally come to a conclusion that there is no real difference between operators and functions. Or to say more precisely - operators are functions that use a different calling convention by default.

And that is really true for most of the standard Ela operators. Which again leads us to another conclusion: if a difference is, well, basically non-existent, why not give users the ability to define their own operators? And this is possible in Ela:

let !! x y = x.[y]
let lst = [| 1..10 |]
let res = lst !! 2 //take the element with index 2 from the array

Type system

Ela comes with a rich and extensible type system out of the box. It supports four numeric types, strings, chars, immutable linked lists, arrays, tuples, records, variants, and more.

Numeric types include 32-bit and 64-bit integers and 32-bit and 64-bit floating point numbers:

let i4 = 42
let i8 = 42L
let r4 = 12.2
let r8 = 123.12D

Strings and chars use C style literals and escape codes:

let str = "Hello,\r\n\"Dr. Smith\"!"
let ch = str.[0]
let ch2 = '\t'

Single linked lists can be constructed either using list literals or the list construction operator :: similar to the one used in F#:

let lst = [1,2,3]
let lst2 = 1::2::3::[]

Arrays are dynamic indexed arrays that might remind you of the ArrayList class from the .NET Framework library:

let arr = [| 1,2,3 |]
let _ = arr.add 4
let _ = arr.insert 0 100
let _ = arr.removeAt 3

Tuples are flat grouped sequences of elements that are useful if you want, for example, to fetch several values from a function:

let tup = (1,2,(3,4)) //the same as (1,2,3,4)
let res = (1,2) + (3,4) //equals to (4,6)

Records are tuples that provide the possibility to access an element by its name. You can also declare records with mutable fields (which is not possible with tuples):

let rec = { x = 2, mutable y = 42, type = "Variables" }
let _ = rec.y <- 42.42

There are other data types as well. For a more detailed overview of Ela data types, you can refer to this article.

The key thing to know about the Ela type system is that all operations in Ela are abstract and a type system is based on the notion of traits. A trait is somewhat similar to an interface in an object oriented language. Each data type in Ela implements a number of traits that basically describe which operations are available for this type.

As you have seen above, it is absolutely legal to perform arithmetic operations on tuples as tuples support the Num trait. Also, strings in Ela are implemented as indexed arrays (like in .NET), but you can also fold them as lists, thanks to the trait system:

let foldl f z x::xs = foldl f (f z x) xs;
          _ z []    = z

let reverse = foldl (flip (::)) []
let revStr = reverse "Hello, world!"

With traits, you can add, for example, a new numeric type to Ela without a single change to the language implementation.

Polymorphic variants

Polymorphic variants are yet another data type in Ela. Their title might sound somewhat complex, but the concept itself is pretty straightforward. The idea comes from the OCaml language, but the Ela approach is somewhat different.

Polymorphic variants simply give you a way to "tag" a value:

let tagged = `Even 12

You can think of them as a tool that allows you to associate some additional information with a value and to use this information further - for example, in pattern matching:

let res = match tagged with
          `Even x = "The number is even!";
          `Odd x  = "The number is odd!";
          _       = "Something is wrong here..."
          end

The cool thing about variants is that they are fully transparent to the client code - e.g., if you tag an integer, you can still treat a tagged value as an integer and perform calculations with it:

let tagged2 = `Odd 13
let res1 = tagged + tagged2 //The results is 25
let res2 = tagged - 9 //The result is 3

Thunks and lazy lists

Thunks are a tool that allows you to do lazy computations. Thunk is a special expression enclosed in parenthesis with an ampersand that is not evaluated immediately but only when its value is actually needed:

let t = (& 2 + 2)
let res = t * 2

In the example above, the variable t is not initialized with the result of the evaluation of the expression 2 + 2. Instead, Ela creates a hidden closure that contains the above calculation. This closure is called the first time the value of a thunk is needed - after that, the value is memorized and no calculation is performed the next time you refer to the t variable.

You can achieve a similar behavior by passing a function that calculates a value instead of an actual value, but thunks have two distinctive features here: first, they perform memorization when regular functions obviously don't, and second, they can be used transparently in your code as you can see from the code sample above.

It means that you can implement a function that performs some operations with its arguments, such as simple arithmetic, and you can pass to this function either actual values or thunks - they both will perfectly do.

Lazy lists in Ela are constructed with the help of thunks by using a thunk instead of a list tail. Here is a small example of a function that generates an infinite list of integers:

let ints n = nn :: (& ints nn) 
             where nn = n + 1 end

let res = ints 0

Ranges and list comprehensions

Ranges in Ela are arithmetic sequences that can be used to create lists and arrays. You only have to specify a first element, a last element, and (optionally) a second element in a sequence that will be used to calculate stepping:

let r1 = [1..5] //[1,2,3,4,5]
let r2 = [1,3..10] //[1,3,5,7,9]
let r3 = [100,87..1] //[100,87,74,61,48,35,22,9]

You can also generate infinite lists using ranges by omitting the last sequence element:

let inf1 = [1..] //[1,2,3,4,5...]
let inf2 = [3,2..] //[3,2,1,0,-1,-2..]

List and array comprehensions in Ela resemble the classical mathematical set builder notation. They can be used to generate new sequences from existing sequences by providing selection conditions and projection code. You can also pattern match in list comprehensions:

let lst = [x + y @ (x,y) <- [(1,2)..(4,5)] | x > 2] //The result is [7,9]

Here, we are taking a list of tuples, selecting only those where the first element is greater than 2, and returning a new list whose elements are the sum of the tuple elements. Imagine how much code you would need to write to do the same thing in an imperative language.

First class modules

Modules are first class values in Ela - you can assign them to variables, pass as arguments to other functions, and so on:

open Math
open SymbolMath

let doSum x y mod = mod.sum x y
let res1 = Math <| doSum 2 2
let res2 = SymbolicMath <| doSum 2 2

Benchmarks

That is with no doubt the most important part of the article. I will compare Ela with probably one the most popular dynamic languages in the world, with JavaScript. I am using JScript - a JavaScript implementation that comes with Microsoft Internet Explorer 8.

I have spent quite a while thinking what would be the best test for Ela. Ela is functional language, therefore we should have a functional style test. JavaScript also supports the functional programming paradigm, but in comparison with Ela, its support is pretty limited. Also, JavaScript lacks some important (almost mandatory for a functional language) optimizations such as optimization of a tail call recursion. So using tail recursive functions in a test would be probably unfair to JavaScript as it won't show the actual performance of the two languages. That is why I've chosen a test that doesn't use tail recursion at all.

I am using a Core i5 750 CPU and Windows 7. Both JScript and Ela are running in 32-bit mode.

The task is simple - filter a list with 100000 elements using a given predicate. This is the JavaScript code:

var arr = [];

for (var i = 0; i < 1000*100; i++)
    arr[arr.length] = i;

function filter(pred, arr) {
    var newArr = [];
    
    for (x in arr)
    {
        if (pred(x))
            newArr[newArr.length] = x;
    }
    
    return newArr;
}

var result = filter(function(x) { return x % 2 == 0; }, arr);

And the Ela version:

let filter p x::xs | p x  = x :: filter xs;
                   | else = filter xs;
           _ []           = []
           
let res = filter (\x -> x % 2 == 0) [1..1000*100]

And the results:

JavaScript 164 ms
Ela 30 ms

I ran the same test ten times and calculated the average - and that is what you see. Also, as you can probably notice, the JavaScript version of the code tends to be pretty imperative and verbose, when Ela code is concise and functional.

Conclusion

Ela is still under active development - currently, I am working on the documentation and the standard library. The Ela source code is freely available under GPL v2 license, and can be found in the Google Code repository (see the Links section below). You can also download the latest binary releases and read the documentation that describes the language features in more detail. I am always glad to hear any feedback and comments, and help you if you will decide to use Ela in your application.

Links

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"