指令式Callback,函數式Promise:對node.js的一聲嘆息


原文:Callbacks are imperative, promises are functional: Node’s biggest missed opportunity

promises 天生就不會受不斷變化的情況影響。
-- Frank Underwood, ‘House of Cards’

人們常說Javascript是'函數式'編程語言。而這僅僅因為函數是它的一等值,可函數式編程的很多其他特性,包括不可變數據,遞歸比循環更招人待見,代數類型系統,規避副作用等,它都不俱備。盡管把函數作為一等公民確實管用,也讓碼農可以根據自己的需要決定是否采用函數式的風格編程,但宣稱JS是函數式的往往會讓JS碼農們忽略函數式編程的一個核心理念:用值編程。

'函數式編程'是一個使用不當的詞,因為它會讓人們以為這是'用函數編程'的意思,把它跟用對象編程相對比。但如果面向對象編程是把一切都當作對象,那函數式編程是把一切都當作值,不僅函數是值,而是一切都是值。這其中當然包括顯而易見的數值、字符串、列表和其它數據,還包括我們這些OOP狗一般不會看成值的其它東西:IO操作和其它副作用,GUI事件流,null檢查,甚至是函數調用序列的概念。如果你曾聽說過'可編程的分號'1這個短語,你應該就能明白我在說什么了。

1單子。 In functional programming, a monad is a structure that represents computations. A type with a monad structure defines what it means to chain operations of that type together. This allows the programmer to build pipelines that process data in steps, in which each action is decorated with additional processing rules provided by the monad. As such, monads have been described as "programmable semicolons"; a semicolon is the operator used to chain together individual statements in many imperative programming languages, thus the expression implies that extra code will be executed between the statements in the pipeline. Monads have been also explained with a physical metaphor as assembly lines, where a conveyor belt transports data between functional units that transform it one step at a time. http://en.wikipedia.org/wiki/Monad_(functional_programming)

最好的函數式編程是聲明式的。在指令式編程中,我們編寫指令序列來告訴機器如何做我們想做的事情。在函數式編程中,我們描述值之間的關系,告訴機器我們想計算什么,然后由機器自己產生指令序列完成計算。

用過excel的人都做過函數式編程:在其中通過建模把一個問題描繪成一個值圖(如何從一個值推導出另一個)。當插入新值時,Excel負責找出它對圖會產生什么影響,並幫你完成所有的更新,而無需你編寫指令序列指導它完成這項工作。

有了這個定義做依據,我要指出node.js一個最大的設計失誤,最起碼我是這樣認為的:在最初設計node.js時,在確定提供哪種方式的API式,它選擇了基於callback,而不是基於promise。

所有人都在用 [callbacks]。如果你發布了一個返回promise的模塊,沒人會注意到它。人們甚至不會去用那樣一個模塊。

如果我要自己寫個小庫,用來跟Redis交互,並且這是它所做的最后一件事,我可以把傳給我的callback轉給Redis。而且當我們真地遇到callback hell之類的問題時,我會告訴你一個秘密:這里還有協同hell和單子hell,並且對於你所創建的任何抽象工具,只要你用得足夠多,總會遇到某個hell。

在90%的情況下我們都有這種超級簡單的接口,所以當我們需要做某件事的時候,只要小小的縮進一下,就可以搞定了。而在遇到復雜的情況時,你可以像npm里的其它827個模塊一樣,裝上async。

--Mikeal Rogers, LXJS 2012

Node宣稱它的設計目標是讓碼農中的屌絲也能輕松寫出反應迅速的並發網絡程序,但我認為這個美好的願望撞牆了。用Promise可以讓運行時確定控制流程,而不是讓碼農絞盡腦汁地明確寫出來,所以更容易構建出正確的、並發程度最高的程序。

編寫正確的並發程序歸根結底是要讓盡可能多的操作同步進行,但各操作的執行順序仍能正確無誤。盡管Javascript是單線程的,但由於異步,我們仍然會遇到競態條件:所有涉及到I/O操作的操作在等待callback時都要把CPU時間讓給其他操作。多個並發操作都能訪問內存中的相同數據,對數據庫或DOM執行重疊的命令序列。借助promise,我們可以像excel那樣用值之間的相互關系來描述問題,從而讓工具幫你找出最優的解決方案,而不是你親自去確定控制流。

我希望澄清大家對promise的誤解,它的作用不僅是給基於callback的異步實現找一個語法更清晰的寫法。promise以一種全新的方式對問題建模;它要比語法層面的變化更深入,實際上是在語義層上改變了解決問題的方式。

我在兩年前曾寫過一篇文章,promises是異步編程的單子。那篇文章的核心理念是單子是組建函數的工具,比如構建一個以上一個函數的輸出作為下一個函數輸入的管道。這是通過使用值之間的結構化關系來達成的,它的值和彼此之間的關系在這里仍要發揮重要作用。

我仍將借助Haskell的類型聲明來闡明問題。在Haskell中,聲明foo::bar表示“foo是類型為bar的值”。聲明foo :: Bar -> Qux 的意思是"foo是一個函數,以類型Bar的值為參數,返回一個類型為Qux的值"。如果輸入/輸出的確切類型無關緊要,可以用單個的小寫字母表示,foo :: a -> b。如果foo的參數不止一個,可以加上更多的箭頭,比如foo :: a -> b -> c表示foo有兩個類型分別為a和b的參數,返回類型為c的值。

我們來看一個Node函數,就以fs.readFile()為例吧。這個函數的參數是一個String類型的路徑名和一個callback函數,它沒有任何返回值。callback函數有兩個參數,Error(可能為null)和包含文件內容的Buffer,也是沒有任何返回值。我們可以把readFile的類型表示為:

readFile :: String -> Callback -> ()

() 在 Haskell 中表示 null 類型。callback 本身是另一個函數,它的類型簽名是:

Callback :: Error -> Buffer -> ()

把這些都放到一起,則可以說readFile以一個String和一個帶着Buffer調用的函數為參數:

readFile :: String -> (Error -> Buffer -> ()) -> ()

好,現在請想象一下Node使用promises是什么情況。對於readFile而言,就是簡單地接受一個String類型的值,並返回一個Buffer的promise值。

readFile :: String -> Promise Buffer

說得更概括一點,就是基於callback的函數接受一些輸入和一個callback,然后用它的輸出調用這個callback函數,而基於promise的函數接受輸入,返回輸出的promise值:

callback :: a -> (Error -> b -> ()) -> ()
promise :: a -> Promise b

基於callback的函數返回的那些null值就是基於callback編程之所以艱難的源頭:基於callback的函數什么都不返回,所以難以把它們組裝到一起。沒有返回值的函數,執行它僅僅是因為它的副作用 -- 沒有返回值或副作用的函數就是個黑洞。所以用callback編程天生就是指令式的,是編寫以副作用為主的過程的執行順序,而不是像函數應用那樣把輸入映射到輸出。是手工編排控制流,而不是通過定義值之間的關系來解決問題。因此使編寫正確的並發程序變得艱難。

而基於promise的函數與之相反,你總能把函數的結果當作一個與時間無關的值。在調用基於callback的函數時,在你調用這個函數和它的callback被調用之間要經過一段時間,而在這段時間里,程序中的任何地方都找不到表示結果的值。

fs.readFile('file1.txt',
  // 時光流逝...
  function(error, buffer) {
    // 現在,結果突然跌落在凡間
  }
);

從基於callback或事件的函數中得到結果基本上就意味着你“要在正確的時間正確的地點”出現。如果你是在事件已經被觸發之后才把事件監聽器綁定上去,或者把callback放錯了位置,那上帝也罩不了你,你只能看着結果從眼前溜走。這對於用Node寫HTTP服務器的人來說就像瘟疫一樣。如果你搞錯了控制流,那你的程序就只能崩潰。

而Promises與之相反,它不關心時間或者順序。無論你在promise被resolve之前還是之后附上監聽器,都沒關系,你總能從中得到結果值。因此,返回promises的函數馬上就能給你一個表示結果的值,你可以把它當作一等數據來用,也可以把它傳給其它函數。不用等着callback,也不會錯過任何事件。只要你手中握有promise,你就能從中得到結果值。

var p1 = new Promise();
p1.then(console.log);
p1.resolve(42);

var p2 = new Promise();
p2.resolve(2013);
p2.then(console.log);

// prints:
// 42
// 2013

所以盡管then()這個方法的名字讓人覺得它跟某種順序化的操作有關,並且那確實是它所承擔的職責的副產品,但你真的可以把它當作unwrap來看待。promise是一個存放未知值的容器,而then的任務就是把這個值從promise中提取出來,把它交給另一個函數:從單子的角度來看就是bind函數。在上面的代碼中,我們完全看不出來該值何時可用,或代碼執行的順序是什么,它只表達了某種依賴關系:要想在日志中輸出某個值,那你必須先知道這個值是什么。程序執行的順序是從這些依賴信息中推導出來的。兩者的區別其實相當微妙,但隨着我們討論的不斷深入,到文章末尾的lazy promises時,這個區別就會變得愈加明顯。

到目前為止,你看到的都是些無足輕重的東西;一些彼此之間幾乎沒什么互動的小函數。為了讓你了解promises為什么比callback更強大,我們來搞點更需要技巧性的把戲。假設我們要寫段代碼,用fs.stat()取得一堆文件的mtimes屬性。如果這是異步的,我們只需要調用paths.map(fs.stat),但既然跟異步函數映射難度較大,所以我們把async模塊挖出來用一下。

var async = require('async'),
    fs    = require('fs');

var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

async.map(paths, fs.stat, function(error, results) {
  // use the results
});

(哦,我知道fs的函數都有sync版本,但很多其它I/O操作都沒有這種待遇。所以,請淡定地坐下來看我把戲法變完。)

一切都很美好,但是,新需求來了,我們還需要得到file1的size。只要再stat就可以了:

var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

async.map(paths, fs.stat, function(error, results) {
  // use the results
});

fs.stat(paths[0], function(error, stat) {
  // use stat.size
});

需求滿足了,但這個跟size有關的任務要等着前面整個列表中的文件都處理完才會開始。如果前面那個文件列表中的任何一項出錯了,很不幸,我們根本就不可能得到第一個文件的size。這可就大大地壞了,所以,我們要試試別的辦法:把第一個文件從文件列表中拿出來單獨處理。

var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
    file1 = paths.shift();

fs.stat(file1, function(error, stat) {
  // use stat.size
  async.map(paths, fs.stat, function(error, results) {
    results.unshift(stat);
    // use the results
  });
});

這樣也行,但現在我們已經不能把這個程序稱為並行化的了:它要用更長的時間,因為在處理完第一個文件之前,文件列表的請求處理得一直等着。之前它們還都是並發運行的。另外我們還不得不處理下數組,以便可以把第一個文件提出來做特別的處理。

Okay,最后的成功一擊。我們知道需要得到所有文件的stats,每次命中一個文件,如果成功,則在第一個文件上做些工作,然后如果整個文件列表都成功了,則要在那個列表上做些工作。帶着對問題中這些依賴關系的認識,用async把它表示出來。

var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
    file1 = paths.shift();

async.parallel([
  function(callback) {
    fs.stat(file1, function(error, stat) {
      // use stat.size
      callback(error, stat);
    });
  },
  function(callback) {
    async.map(paths, fs.stat, callback);
  }
], function(error, results) {
  var stats = [results[0]].concat(results[1]);
  // use the stats
});

這就對了:每次一個文件,所有工作都是並行的,第一個文件的結果跟其他的沒關系,而相關任務可以盡早執行。Mission accomplished!

好吧,實際上並不盡然。這個太丑了,並且當問題變得更加復雜后,這個顯然不易於擴展。為了正確解決問題,要考慮很多東西,而且這個設計意圖也不顯眼,后期維護時很可能會把它破壞掉,后續任務跟如何完成所需工作的策略混雜在一起,而且我們不得不動用一些比較復雜的數組分割操作來應對這個特殊狀況。啊哦!

這些問題的根源都在於我們用控制流作為解決辦法的主體,如果用數據間的依賴關系,就不會這樣了。我們的思路不是“要運行這個任務,我需要這個數據”,沒有把找出最優路徑的工作交給運行時,而是明確地向運行時指出哪些應該並行,哪些應該順行,所以我們得到了一個特別脆弱的解決方案。

那promises怎么幫你脫離困境?嗯,首先要有能返回promises的文件系統函數,用callback做參數的那套東西不行。但在這里我們不要手工打造一套文件系統函數,通過元編程作個能轉換一切函數的東西就行。比如,它應該接受類型為:

String -> (Error -> Stat -> ()) -> ()

的函數,並返回:

String -> Promise Stat

下面就是這樣一個函數:

// promisify :: (a -> (Error -> b -> ()) -> ()) -> (a -> Promise b)
var promisify = function(fn, receiver) {
  return function() {
    var slice   = Array.prototype.slice,
        args    = slice.call(arguments, 0, fn.length - 1),
        promise = new Promise();

    args.push(function() {
      var results = slice.call(arguments),
          error   = results.shift();

      if (error) promise.reject(error);
      else promise.resolve.apply(promise, results);
    });

    fn.apply(receiver, args);
    return promise;
  };
};

(這不是特別通用,但對我們來說夠了.)

現在我們可以對問題重新建模。我們需要做的全部工作基本就是將一個路徑列表映射到一個stats的promises列表上:

var fs_stat = promisify(fs.stat);

var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

// [String] -> [Promise Stat]
var statsPromises = paths.map(fs_stat);

這已經是付利息了:在用 async.map()時,在整個列表處理完之前你拿不到任何數據,而用上promises的列表之后,你可以徑直挑出第一個文件的stat做些處理:

statsPromises[0].then(function(stat) { /* use stat.size */ });

所以在用上promise值后,我們已經解決了大部分問題:所有文件的stat都是並發進行的,並且訪問所有文件的stat都和其他的無關,可以從數組中直接挑我們想要的任何一個,不止是第一個了。在前面那個方案中,我們必須在代碼里明確寫明要處理第一個文件,想換文件時改起來不是那么容易,但用promises列表就容易多了。

謎底還沒有完全揭曉,在得到所有的stat結果之后,我們該做什么?在之前的程序中,我們最終得到的是一個Stat對象的列表,而現在我們得到的是一個Promise Stat 對象的列表。我們想等着所有這些promises都被兌現(resolve),然后生出一個包含所有stats的列表。換句話說,我們想把一個promises列表變成一個列表的promise。

閑言少敘,我們現在就給這個列表加上promise方法,那這個包含promises的列表就會變成一個promise,當它所包含的所有元素都兌現后,它也就兌現了。

// list :: [Promise a] -> Promise [a]
var list = function(promises) {
  var listPromise = new Promise();
  for (var k in listPromise) promises[k] = listPromise[k];

  var results = [], done = 0;

  promises.forEach(function(promise, i) {
    promise.then(function(result) {
      results[i] = result;
      done += 1;
      if (done === promises.length) promises.resolve(results);
    }, function(error) {
      promises.reject(error);
    });
  });

  if (promises.length === 0) promises.resolve(results);
  return promises;
};

(這個函數跟 jQuery.when() 類似, 以一個promises列表為參數,返回一個新的promise,當參數中的所有promises都兌現后,這個新的promise就兌現了.)

只需把數組打包在promise里,我們就可以等着所有結果出來了:

list(statsPromises).then(function(stats) { /* use the stats */ });

我們最終的解決方案就被削減成了下面這樣:

var fs_stat = promisify(fs.stat);

var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
    statsPromises = list(paths.map(fs_stat));

statsPromises[0].then(function(stat) {
  // use stat.size
});

statsPromises.then(function(stats) {
  // use the stats
});

該方案的這種表示方式看起來要清楚得多了。借助一點通用的粘合劑(我們的promise輔助函數),以及已有的數組方法,我們就能用正確、有效、修改起來非常容易的辦法解決這個問題。不需要async模塊特制的集合方法,只是讓promises和數組兩者的思想各自保持獨立,然后以非常強大的方式把它們整合到一起。

特別要注意這個程序是如何避免了跟並行或順序相關的字眼出現。它只是說我們想做什么,然后說明任務之間的依賴關系是什么樣的,其他的事情就交給promise類庫去做了。

實際上,async集合模塊中的很多東西都可以用promises列表上的操作輕松代替。前面已經看到map的例子了:

async.map(inputs, fn, function(error, results) {});

相當於:

list(inputs.map(promisify(fn))).then(
    function(results) {},
    function(error) {}
);

async.each()async.map() 實質上是一樣的,只不過each()只是要執行效果,不關心返回值。完全可以用map()代替。

async.mapSeries() (如前所述,包括 async.eachSeries()) 相當於在promises列表上調用 reduce()。也就是說,你拿到輸入列表,並用reduce產生一個promise,每個操作都依賴於之前的操作是否成功。我們來看一個例子:基於fs.rmdir()實現 rm -rf 。代碼如下:

var dirs = ['a/b/c', 'a/b', 'a'];
async.mapSeries(dirs, fs.rmdir, function(error) {});

相當於:

var dirs     = ['a/b/c', 'a/b', 'a'],
    fs_rmdir = promisify(fs.rmdir);

var rm_rf = dirs.reduce(function(promise, path) {
  return promise.then(function() { return fs_rmdir(path) });
}, unit());

rm_rf.then(
    function() {},
    function(error) {}
);

其中的 unit()只是為了產生一個已解決的promise已啟動操作鏈(如果你知道monads,這就是給promises的return 函數):

// unit :: a -> Promise a
var unit = function(a) {
  var promise = new Promise();
  promise.resolve(a);
  return promise;
};

reduce()只是取出路徑列表中的每對目錄,用promise.then()根據上一步操作是否成功來執行路徑刪除操作。這樣可以處理非空目錄:如果上一個promise由於某種錯誤被rejecte了,操作鏈就會終止。用值之間的依賴關系限定執行順序是函數式語言借助monads處理副作用的核心思想。

最后這個例子的代碼比async版本繁瑣得多,但不要被它騙了。關鍵是領會精神,要將彼此不相干的promise值和list操作結合起來組裝程序,而不是依賴定制的流程控制庫。如您所見,前一種方式寫出來的程序更容易理解。

准確地講,它之所以容易理解,是因為我們把一部分思考的過程交給機器了。如果用async模塊,我們的思考過程是這樣的:

  • A.程序中這些任務間的依賴關系是這樣的
  • B.因此各操作的順序必須是這樣
  • C.然后我們把B所表達的意思寫成代碼吧

用promises依賴圖可以跳過步驟B。代碼只要表達任務之間的依賴關系,然后讓電腦去設定控制流。換種說法,callback用顯式的控制流把很多細小的值粘到一起,而promises用顯式的值間關系把很多細小的控制流粘到一起。Callback是指令式的,promises是函數式的。

如果最終沒有一個完整的promises應用,並且是體現函數式編程核心思想 laziness的應用,我們對這個話題的討論就不算完整。Haskell是一門懶語言,也就是說它不會把程序當成從頭運行到尾的腳本,而是從定義程序輸出的表達式開始,向stdio、數據庫中寫了什么等等,以此向后推導。它尋找最終表達式的輸入所依賴的那些表達式,按圖反向探索,直到計算出程序產生輸出所需的一切。只有程序為完成任務而需要計算的東西才會計算。

解決計算機科學問題的最佳解決方案通常都是找到可以對其建模的准確數據結構。Javascript有一個與之非常相似的問題:模塊加載。我們只想加載程序真正需要的模塊,而且想盡可能高效地完成這個任務。

在 CommonJS 和 AMD出現之前,我們確實就已經有依賴的概念了,腳本加載庫有一大把。大多數的工作方式都跟前面的例子差不多,明確告訴腳本加載器哪些文件可以並行下載,哪些必須按順序來。基本上都必須寫出下載策略,要想做到正確高效,那是相當困難,跟簡單描述腳本間的依賴關系,讓加載器自己決定順序比起來簡直太坑人了。

接下來開始介紹LazyPromise的概念。這是一個promise對象,其中會包含一個可能做異步工作的函數。這個函數只在調用promise的then()時才會被調用一次:即只在需要它的結果時才開始計算它。這是通過重寫then()實現的,如果工作還沒開始,就啟動它。

var Promise = require('rsvp').Promise,
    util    = require('util');

var LazyPromise = function(factory) {
  this._factory = factory;
  this._started = false;
};
util.inherits(LazyPromise, Promise);

LazyPromise.prototype.then = function() {
  if (!this._started) {
    this._started = true;
    var self = this;

    this._factory(function(error, result) {
      if (error) self.reject(error);
      else self.resolve(result);
    });
  }
  return Promise.prototype.then.apply(this, arguments);
};

比如下面這個程序,它什么也不做:因為我們根本沒要過promise的結果,所以不用干活:

var delayed = new LazyPromise(function(callback) {
  console.log('Started');
  setTimeout(function() {
    console.log('Done');
    callback(null, 42);
  }, 1000);
});

但如果加上下面這行,程序就會輸出Started,過了一秒后,在輸出Done和42:

delayed.then(console.log);

但既然這個工作只做一次,調用then()會多次輸出結構,但並不會每次都執行任務:

delayed.then(console.log);
delayed.then(console.log);
delayed.then(console.log);

// prints:
// Started
// -- 1 second delay --
// Done
// 42
// 42
// 42

用這個非常簡單的通用抽象,我們可以隨時搭建一個優化模塊系統。假定我們要像下面這樣創建一堆模塊:每個模塊都有一個名字,一個依賴模塊列表,以及一個傳入依賴項,返回模塊API的工廠函數。跟AMD的工作方式非常像。

var A = new Module('A', [], function() {
  return {
    logBase: function(x, y) {
      return Math.log(x) / Math.log(y);
    }
  };
});

var B = new Module('B', [A], function(a) {
  return {
    doMath: function(x, y) {
      return 'B result is: ' + a.logBase(x, y);
    }
  };
});

var C = new Module('C', [A], function(a) {
  return {
    doMath: function(x, y) {
      return 'C result is: ' + a.logBase(y, x);
    }
  };
});

var D = new Module('D', [B, C], function(b, c) {
  return {
    run: function(x, y) {
      console.log(b.doMath(x, y));
      console.log(c.doMath(x, y));
    }
  };
});

這里出了一個鑽石的形狀:D依賴於B和C,而它們每個都依賴於A。也就是說我們可以加載A,然后並行加載B和C,兩個都到位后加載D。但是,我們希望工具能自己找出這個順序,而不是由我們自己寫出來。

這很容易實現,我們把模塊當作LazyPromise的子類型來建模。它的工廠只要用我們前面那個list promise輔助函數得到依賴項的值,然后再經過一段模擬的加載時間后用那些依賴項構建模塊。

var DELAY = 1000;

var Module = function(name, deps, factory) {
  this._factory = function(callback) {
    list(deps).then(function(apis) {
      console.log('-- module LOAD: ' + name);
      setTimeout(function() {
        console.log('-- module done: ' + name);
        var api = factory.apply(this, apis);
        callback(null, api);
      }, DELAY);
    });
  };
};
util.inherits(Module, LazyPromise);

因為 Module 是 LazyPromise, 只是像上面那樣定義模塊不會加載。我們只在需要用這些模塊的時候加載它們:

D.then(function(d) { d.run(1000, 2) });

// prints:
// 
// -- module LOAD: A
// -- module done: A
// -- module LOAD: B
// -- module LOAD: C
// -- module done: B
// -- module done: C
// -- module LOAD: D
// -- module done: D
// B result is: 9.965784284662087
// C result is: 0.10034333188799373

如上所示,最先加載的是A,完成后同時開始下載B和C,在兩個都完成后加載D,跟我們想的一樣。如果調用C.then(function() {}),那就只會加載A和C;不在依賴關系圖中的模塊不會加載。

所以我們幾乎沒怎么寫代碼就創建了一個正確的優化模塊加載器,只要用lazy promises的圖就行了。我們用函數式編程中值間關系的方式代替了顯式聲明控制流的方式,比我們自己寫控制流容易得多。對於任何一個非循環得依賴關系圖,這個庫都能用來替你優化控制流。

這就是promises真正強大的地方。它不僅能在語法層面上規避縮進金字塔,還能讓你在更高層次上對問題建模,而把底層工作交給工具完成。真的,那應該是我們所有碼農對我們的軟件提出的要求。如果Node真的想讓並發編程更容易,他們應該再好好看看promises。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM