کاربردِ co و thunkify برای کنترلِ روند با آزانگر (Generator) در جاوا اسکریپت - بخشِ سه
پیش از خواندنِ این نوشته، بخشِ یکم و بخشِ دومِ آن را بخوانید.
در بخشِ پیش دیدیم که چگونه می توانیم اجرای تابعِ آزانگر را کنترل کنیم. تابعی به نامِ run
نوشتیم که می توانست تابع های آزانگر را اجرا کند. می توانستیم آن را توسعه دهیم ولی چندین کتابخانه ی دیگر از پیش نوشته شده اند که کاری همانندِ آن انجام می دهند. یکی از آن ها کتابخانه ای به نامِ co است که در اینجا با آن و یک کتابخانه ی دیگر به نامِ thunkify آشنا می شویم.
کنترلِ روندِ موازی
در بخشِ پیش یک نمونه کدِ ساده را دیدیم. در برنامه هایی که روزانه می نویسیم، برای افزایشِ کارایی، می کوشیم تا کار ها را همزمان با هم و موازی انجام دهیم. بیایید نمونه کدِ بخشِ پیش را کمی پیشرفته تر کنیم. می خواهیم تابعِ readFiles
داشته باشیم که به جای دو مسیرِ پرونده، یک آرایه از آن ها را می گیرد. کدِ زیر را ببینید.
var fs = require('fs')
function readFile(path, done) {
fs.readFile(path, 'utf-8', done)
}
function readFiles(paths, done) {
var count = paths.length
, result = new Array(count)
, doneCalled = false
paths.map(function (path, i) {
readFile(path, function (err, data) {
if (doneCalled) return
if (err) {
doneCalled = true
done(err)
return
}
result[i] = data
count -= 1
if (count === 0)
done(null, result)
})
})
}
readFiles(['md/file1.md', 'md/file2.md', 'md/file3.md'], function (err, res) {
if (err) return console.error(err.message)
res.map(function (data) {
console.log(data)
})
})
همان گونه که می بینید، کار کمی پیچیده تر شده است. تابعِ map
که روی آرایه ها تعریف شده، تابعی که به آن فرستاده می شود را روی همه ی داده های آرایه اجرا می کند. از آنجایی که پاسخِ خواندنِ پرونده ها به ترتیب نخواهد بود، برای هر پاسخ یکی از count
کم می کنیم تا به صفر برسد. سپس اگر صفر شد، done
را فرا می خوانیم. پاسخ های باز گشته را هم در آرایه ی result
در سر جای خود گذاشته ایم.
کنترلِ خطا خیلی پیچیده تر شده است. اگر خطایی در زمانِ خواندنِ هر یک از پرونده ها پیش آید، دیگر نیازی به ادامه نیست و همان جا باید خطا را باز گردانیم. doneCalled
برای این گذاشته شده که اگر پیش تر خطایی رخ داده و done
فراخوانده شده، دیگر فراخوانده نشود. اگر این کار را نکنیم، callback
شاید بیش از یک بار اجرا شود که درست نیست.
همین کارِ ساده، خیلی برنامه ی ساده ی ما را پیچیده کرده و دیگر به سادگی خوانده و فهمیده نمی شود.
به کار گیریِ co
co یکی از کتابخانه های توانمندِ کنترلِ روندِ اجرا است که بدست TJ Holowaychuk نوشته شده است. این کتابخانه یک تابع به نامِ co
دارد که کاری همانند run
در بخشِ پیش انجام می دهد ولی بسیار پیشرفته تر شده است و کار های موازی را هم انجام می دهد. بهتر است نمونه کدِ بالا را با آن بنویسیم تا ببینیم که برای ما چه می کند.
var fs = require('fs')
, co = require('co')
function readFile(path) {
return function (done) {
fs.readFile(path, 'utf-8', done)
}
}
function readFiles(paths, done) {
co(function* () {
return yield paths.map(readFile)
})(done)
}
readFiles(['md/file1.md', 'md/file2.md', 'md/file3.md'], function (err, res) {
if (err) return console.error(err.message)
res.map(function (data) {
console.log(data)
})
})
کد بسیار ساده شده است. در اینجا paths.map(readFile)
تابعِ readFile
را روی تک تکِ اندیس های آرایه فرا می خواند که این تابع خود یک تابع دیگر باز می گرداند و paths.map
همه ی آن ها را در یک آرایه ی دیگر می ریزد و باز می گرداند. پس با اجرای آن، به یک آرایه از تابع ها می رسیم که این تابع ها از readFile
باز گشته اند.
سپس این آرایه به yield
می رسد که آن هم این آرایه را می آزاند و co
آن را می گیرد. co
هر گاه که به یک آرایه یا یک object
از تابع ها برسد، آن ها را موازی اجرا می کند و در پایانِ اجرای همه ی آن ها، نتیجه را به درونِ تابعِ آزانگر باز می گرداند و جایگزینِ yield
می کند یا اگر خطایی رخ دهد، همان خطا را در جای yield
پرتاب می کند. پس اگر خطایی رخ ندهد، به یک آرایه از داده های درونِ پرونده ها می رسیم. سپس return
نیز آن را باز می گرداند. co
این آرایه ی باز گشته را می گیرد و آن را به تابعِ done
که به آن فرستاده شده می فرستد. اگر خطایی رخ دهد که catch
نشده باشد، done
را با آن فرا می خواند.
می بینید که کار با co
خیلی ساده است و چه اندازه کد را ساده تر کرده است. می توانید بگویید که کتابخانه های دیگری هم هستند که کدِ نمونه ی نخست را ساده تر می کنند (مانند async) ولی باز در کنترلِ خطا به co
نمی رسند.
تابعِ آزانگری که به co
فرستاده می شود، می تواند چند چیز را بیازاند. می تواند یک تابعِ آزانگرِ دیگر باشد یا اینکه یک آزانگر باشد. آرایه را هم که در نمونه کدِ بالا دیدیم. اگر object
باشد هم باز کار را موازی پیش می برد. همچنین می توانید یک promise
را به آن بفرستید. یا بهتر از آن، می توانید یک thunk
را به آن بفرستید.
thunk
چیست؟
به تابعی که بیشتر خودکار ساخته می شود تا به فراخوانیِ یک تابعِ دیگر کمک کند، thunk
می گویند. بیشتر برای این به کار می روند که کمی فراخوانیِ تابعِ دیگر را ساده تر کنند.
یک نمونه از آن را کمی بالا تر دیدیم. در اینجا دوباره آورده شده است.
function readFile(path) {
return function (done) {
fs.readFile(path, 'utf-8', done)
}
}
در اینجا readFile
یک thunk
است که تنها فراخوانیِ fs.readFile
را برای ما ساده کرده است. بیشتر تابع ها در Node.js
یک callback
می گیرند که اگر فراخوانده شود، دو پارامتر دارد: (error, response)
. ولی co
برای اینکه بتواند کار کند، نیاز دارد تا تابعی برای آن آزانده شود که تنها یک callback
می گیرد که همین دو پارامتر را می گیرد و نه چیزِ دیگری. برای همین readFile
را نوشتیم تا بتوانیم آن را به جای fs.readFile
به co
بفرستیم.
با thunkify
آشنا شوید
thunkify یک کتابخانه یِ دیگر است که آن هم بدستِ TJ Holowaychuk نوشته شده و کارِ بسیار ساده ای می کند. با آن می توانید به سادگی thunk
برای تابع های Node.js
که callback
می گیرند بسازید. با به کار گیری آن، دیگر نیازی به نوشتنِ تابعِ کدِ پیش نیست.
var readFile = thunkify(fs.readFile)
این همان کارِ کدِ پیش را می کند.
برنامه ی نمونه
بیایید یک نمونه کدِ پیشرفته تر را ببینیم. بیایید بگوییم می خواهیم همه ی پرونده های با غالبِ markdown
را که در پوشه ی md/
هستند به html
تبدیل کنیم و آن ها را در پوشه ی html/
بریزیم. کدِ زیر این کار را با callback
ها انجام می دهد.
var fs = require('fs')
, marked = require('marked')
function convert(inDir, outDir, done) {
fs.readdir(inDir, function (err, files) {
if (err) return done(err)
var count = files.length
, doneCalled = false
, htmlData = new Array(count)
files.map(function (file, i) {
fs.readFile(inDir + file, 'utf-8', function (err, data) {
if (doneCalled) return
if (err) {
doneCalled = true
done(err)
return
}
htmlData[i] = marked(data)
fs.writeFile(outDir + file + '.html', htmlData[i], function (err) {
if (doneCalled) return
if (err) {
doneCalled = true
done(err)
return
}
count -= 1
if (count === 0) done(null, htmlData)
})
})
})
})
}
convert('md/', 'html/', function (err, res) {
if (err) return console.error(err.message)
console.log('%d files converted', res.length)
})
در اینجا marked
کارِ تبدیلِ markdown
به html
را انجام می دهد. با fs.readdir
پرونده های درونِ پوشه ی md/
را خوانده ایم. سپس همانندِ پیش برای آن که بتوانیم پرونده ها را موازی بخوانیم، چند متغیر گرفته ایم. پرونده ها را با fs.readFile
خوانده ایم. کنترلِ خطا کرده ایم. سپس هر پرونده را به html
تبدیل کرده ایم و پس از آن هم آن ها را در پوشه ی html/
ذخیره کرده ایم و باز هم در پایان کنترلِ خطا کرده ایم و سر انجام done
را فراخوانده ایم.
اکنون همین کار را با co
و thunkify
انجام می دهیم.
var fs = require('fs')
, marked = require('marked')
, co = require('co')
, thunkify = require('thunkify')
, readDir = thunkify(fs.readdir)
, readFile = thunkify(fs.readFile)
, writeFile = thunkify(fs.writeFile)
function convert(inDir, outDir, done) {
co(function* () {
var files, filesData, htmlData
files = yield readDir(inDir)
filesData = yield files.map(function (file) {
return readFile(inDir + file, 'utf-8')
})
htmlData = filesData.map(function (data) { return marked(data)})
yield files.map(function (file, i) {
return writeFile(outDir + file + '.html', htmlData[i])
})
return htmlData
})(done)
}
convert('md/', 'html/', function (err, res) {
if (err) return console.error(err.message)
console.log('%d files converted', res.length)
})
می بینید که در آغاز thunk
های readDir
و readFile
و writeFile
را ساخته ایم. سپس در تابعِ آزانگری که به co
فرستاده شده، نخست فهرست پرونده های درونِ پوشه ی md/
را خوانده ایم. سپس داده های پرونده ها را به دست آورده ایم. سپس همه را به html
تبدیل کرده ایم و پس از آن همه را در پوشه ی html/
ذخیره کرده ایم. سرانجام هم html
ها را باز گردانده ایم که co
آن را به done
می دهد. اگر خطایی هم رخ دهد، آن را به done
می دهد.
این دو نمونه ی بالا را با هم مقایسه کنید. نمونه کدِ با callback
خیلی زود نا خوانا می شود و کنترلِ خطا هم در آن خیلی دشوار تر است و شاید خیلی ساده چیزی را در آن میان فراموش کنید. دیگر کاری که برنامه می کند به سادگی فهمیده نمی شود.
کدی که با آزانگر نوشته شده، خیلی سر راست تر، ساده تر و فهمیدنی تر است - اگر آزانگر ها را بدانید. روندِ برنامه مشخص است و کنترلِ خطا هم ساده است. اگر بخواهید می توانید درونِ همین تابعِ آزانگر try/catch
بگذارید و خطاهایی که درونِ هر یک از thunk
ها رخ دهد را بگیرید و کاری برای آن بکنید.
سر انجام
امیدوارم تا اینجا آزانگر ها را آموخته باشید. آزانگر ها که تواناییِ نگه داشتنِ تابع در میانه ی اجرای آن را می دهند، کنترلِ روند را بسیار ساده می کنند. به کار گیریِ Node.js
را هم خیلی ساده تر می کنند. در بخش های آینده با چند ابزارِ دیگر هم آشنا خواهید شد.