بیشترِ نرم افزار هایی که فارسی هستند، نیاز دارند تا زمان را به تاریخِ ایران نشان دهند. برای این کار چندین ابزار و کتابخانه به زبان های گوناگون نوشته شده است. سالِ گذشته، زمانی که روی یک نرم افزار کار می کردم، خیلی به دنبالِ یک کتابخانه ی تاریخ گشتم ولی هیچ کدام نیاز هایم را براورده نکرد. بیشترِ آن ها الگوریتمِ نادرستی داشتند. برخی تنها در یک بازه ی زمانیِ کوتاه که کمتر از ۱۰۰ سال بود کار می کردند. برخی مشکلِ پروانه (License) داشتند. سرانجام تصمیم گرفتم خودم دست به کار شوم و گاهشماریِ جلالی را ساختم.
الگوریتم
الگوریتم های فراوانی نوشته شده ولی بیشترِ آن ها در یک دوره ی زمانیِ کوتاه کار می کنند. پس از کمی جستجو، الگوریتمی را پیدا کردم که در یک بازه ی زمانیِ ۳۰۰۰ ساله کار می کند! این الگوریتم را کازیمیرز برکوسکی نزدیک به ۲۰ سالِ پیش نوشته است و کارایی بسیار بالایی هم دارد. تنها مشکل این بود که الگوریتم به زبانِ Fortran نوشته شده بود.
این الگوریتم یک تابع به نامِ jalCal
دارد که مشخص می کند که آیا یک سالِ جلالی، کبیسه است یا نه. همچنین چهار تابعِ کمکیِ دیگر دارد. j2d
یک تاریخِ جلالی را به روزِ ژولیوسی (Julian Day) تبدیل می کند. d2j
وارونِ این کار را انجام می دهد. g2d
یک تاریخِ میلادی را به روزِ ژولیوسی تبدیل می کند. d2g
هم وارونِ آن را انجام می دهد.
jalaali-js
با بازنویسیِ الگوریتم به زبانِ جاوااسکریپت، jalaali-js ساخته شد. این پروژه الگوریتمِ گفته شده را پیاده می کند و می توانید آن را در مرورگر ها یا برنامه های Node.js به کار برید. افزون بر تابع های گفته شده در الگوریتم، این کتابخانه چندین تابعِ کمکی دیگر هم دارد که کار را ساده تر می کنند.
toJalaali
سال، ماه و روزِ میلادی را می گیرد و سال، ماه و روزِ جلالی را بر می گرداند. toGregorian
هم وارونِ آن را انجام می دهد. isValidJalaaliDate
هم درستیِ یک تاریخِ جلالی را بررسی می کند.
این کتابخانه خیلی کوچک است و برای کاربرد های ساده است. برای نمونه، در وبلاگ خودم برای نمایشِ تاریخِ نوشته ها، این کتابخانه را به کار برده ام و پس از باز شدن وبلاگ در مرورگرِ بیننده، یک اسکریپت اجرا می شود که تاریخ های میلادی را به جلالی تبدیل می کند.
moment-jalaali
یک کتابخانه ی بسیار کاربردی در زبانِ جاوااسکریپت، momentjs است. این کتابخانه، ابزارِ بسیار توانمندی است. برای آشنایی بیشتر با آن پایگاه اش را ببینید. moment-jalaali یک افزونه برای moment است و با آن می توانید همزمان با هر دو تاریخِ میلادی و جلالی کار کنید. با این افزونه، این کتابخانه بسیار توانمند تر شده و می توانید کارهای گوناگونی انجام دهید.
با آن می توانید روز، ماه یا سال به یک تاریخ بیافزایید یا از آن بکاهید. برای نمونه اگر ۶ ماهِ جلالی به ۱۳۹۳/۶/۳۱ بیافزایید، به درستی به تاریخِ ۱۳۹۳/۱۲/۲۹ می رسید. ولی اگر ۱۸۰ روز به آن بیافزایید به تاریخِ ۱۳۹۴/۱/۱ می رسید.
این کتابخانه، تابع هایی مانند add
و subtract
برای افزودن و کاستنِ زمان دارد. همچنین تابع هایی مانندِ startOf
و endOf
دارد که آغاز یا پایانِ یک سال، ماه، روز، ساعت و ... را می دهند. برای دیدن دیگر تابع ها، moment و moment-jalaali را ببینید.
jalaali-hs
چندی پیش زبانِ Haskell را می آموختم و سپس این الگوریتم را با آن زبان نوشتم و jalaali-hs ساخته شد. این پروژه همانندِ jalaali-js است و همان تابع ها را دارد.
jalaali-swift
به تازگی نیز زبانِ Swift را آموختم و این الگوریتم را با آن نیز نوشتم و jalaali-swift ساخته شد. برای ساخت برنامه های iOS و OS X بسیار کمک خواهد کرد.
پروانه
برای این که همه بتوانند به سادگی این ابزار ها را به کار برند، همه ی این کتابخانه ها با پروانه ی MIT منتشر شده اند. با این پروانه شما می توانید هر کاری که می خواهید با این ابزار ها بکنید.
بپیوندید
در GitHub یک سازمان به نامِ jalaali ساخته ام و همه ی این کتابخانه ها زیرِ آن هستند. نشانی آن هم github.com/jalaali است. می توانید به این پروژه بپیوندید و با نوشتن این الگوریتم از روی یکی از همین ابزارها به زبان های دیگر، مشکلِ گاهشماریِ جلالی را از میان ببریم.
برای نمونه، اگر بخواهیم یک گزارش از پایگاه داده ی MySQL بگیریم که با سال و ماهِ جلالی گروه بندی شده باشد، کار بسیار دشواری خواهد بود. اگر این الگوریتم را با زبانِ SQL بنویسیم می توانیم این کار را به سادگی در خودِ پایگاه داده ی MySQL انجام دهیم.
پیش از خواندنِ این نوشته، بخشِ یکم، بخشِ دومِ و بخشِ سومِ آن را بخوانید.
در بخشِ پیش با co و thunkify آشنا شدیم و دیدیم که چگونه با به کار گیریِ آزانگر در کنترلِ روند به ما کمک می کنند. در این جا با ابزاری به نامِ koa آشنا می شویم که برای نوشتنِ وب سرور ها با به کار گیریِ آزانگر ها به کار می رود و می تواند جایگزینی برای ابزارِ پر کاربردِ express باشد.
یک برنامه ی نمونه با express
بیایید نخست یک برنامه ی نمونه که با express
نوشته شده را ببینیم و سپس بکوشیم آن را با koa
بنویسیم. برنامه ی نمونه ی ما خیلی ساده است. کاری که می کند این است که اگر به ریشه (/
) برویم، فهرستی از پرونده های markdown
که در پوشه ی md/
هستند را نشان می دهد و اگر روی هر کدام از آن ها کلیک کنیم، آن پرونده ی markdown
را به html
تبدیل کرده و نمایش می دهد. کدِ آن را در زیر ببینید.
var fs = require('fs')
, marked = require('marked')
, express = require('express')
, app = express()
, port = 3000
app.get('/', list)
app.get('/:filename', show)
app.listen(port, function () {
console.log('listening on port %s', port)
})
function list(req, res, next) {
fs.readdir('md/', function (err, files) {
if (err) return next(err)
res.send(renderList(files))
})
}
function show(req, res, next) {
var filename = req.params.filename
fs.readFile('md/' + filename, 'utf-8', function (err, content) {
if (err) return next(err)
res.send(marked(content))
})
}
function renderList(filenames) {
var links = filenames.map(function (filename) {
return '* [' + filename + '](' + filename + ')'
})
return marked('#Markdown files\n' + links.join('\n'))
}
برنامه ی بسیار ساده ای است. app
یک برنامه ی express
است که به درگاهِ 3000
گوش می کند. اگر به /
برویم، تابعِ list
فراخوانی می شود که فهرست پرونده های پوشه ی md
را به دست می آورد و سپس نتیجه ی renderList
را نشان می دهد. renderList
فهرست پرونده ها را تبدیل به یک پرونده ی markdown
می کند که پیوند هایی به هر پرونده دارد و سپس آن را تبدیل به html
می کند.
اگر به /:filename
برویم (که filename
در اینجا نام هر پرونده ی پوشه ی md
خواهد بود)، تابعِ show
فراخوانی می شود که پرونده را خوانده و آن را تبدیل به html
می کند و نمایش می دهد.
koa
چیست؟
سازنده ی express
همان TJ Holowaychuk
است که پس از نوشتنِ co
و thunkify
برای به کار گیریِ آزانگر ها در نوشتنِ وب سرور ها، koa را ساخت. koa
به سادگیِ express
است ولی با داشتنِ خوبی های آزانگر ها.
برنامه ی نمونه با koa
اکنون همان برنامه را با koa
می نویسیم. فراموش نکنید که برای اجرای آن به نسخه ی v0.11.x
از node
نیاز دارید و باید --harmony
را به آن بفرستید. کدِ زیر را ببینید.
var fs = require('fs')
, marked = require('marked')
, koa = require('koa')
, app = koa()
, port = 3000
, route = require('koa-route')
, thunkify = require('thunkify')
, readDir = thunkify(fs.readdir)
, readFile = thunkify(fs.readFile)
app.use(route.get('/', list))
app.use(route.get('/:filename', show))
app.listen(port, function () {
console.log('listening on port %s', port)
})
function* list() {
var files = yield readDir('md/')
this.body = renderList(files)
}
function* show(filename) {
var content = yield readFile('md/' + filename, 'utf-8')
this.body = marked(content)
}
function renderList(filenames) {
var links = filenames.map(function (filename) {
return '* [' + filename + '](' + filename + ')'
})
return marked('#Markdown files\n' + links.join('\n'))
}
به جای express
در اینجا koa
را آورده ایم. koa-route کاری همانند route
ها در express
می کند که در koa
نیست و باید آن را جداگانه بیاوریم. marked
و thunkify
و readDir
و readFile
هم در بخش پیش گفته شدند.
تابعِ renderList
همانند پیش است. route
ها اندکی تغییر کرده اند ولی مشخص است که چه می کنند. بیشترین تغییر را list
و show
دارند. این دو دیگر تابع نیستند، تابعِ آزانگر هستند.
list
یک تابعِ آزانگر است که نخست فهرستِ پرونده های پوشه ی md
را به دست می آورد. سپس آن ها را به renderList
می دهد و نتیجه را در this.body
می گذارد. در koa
دیگر از پارامتر های req
و res
و next
خبری نیست. برای دسترسی به request
و response
باید با context
کار کنید که برای دسترسی به آن this
را به کار می برید. در اینجا برای نوشتنِ نتیجه تنها باید آن را در this.body
بگذاریم. با این کار koa
نتیجه را به درخواست کننده باز می گرداند.
show
هم یک تابعِ آزانگر شده که یک پارامتر به نامِ filename
می گیرد. koa-route
این پارامتر را از نشانیِ درخواست گرفته و به show
می فرستد. هر بار که اجرا شود، پرونده را می خواند و آن را تبدیل به html
می کند و سپس نتیجه را در this.body
می گذارد که koa
آن را به درخواست کننده باز می گرداند.
چه به دست آوردیم؟
نمونه کدی که با koa
نوشته شده دو ویژگیِ خیلی مهم در برابرِ express
دارد. ویژگیِ نخست گرفتنِ خطا ها است. همان گونه که در بخش های پیش گفته شد، گرفتنِ خطا در اینجا خیلی ساده است و با try/catch
به سادگی می توان خطا را کنترل کرد.
ویژگی دوم هم این است که کد خیلی خوانا تر شده. list
یا show
را که کارِ اصلیِ برنامه را انجام می دهند در هر دو نمونه کد با هم مقایسه کنید. در نمونه کدِ دوم روندِ برنامه مشخص است و با یک نگاه خوانده می شود. ولی در نمونه کدِ یک خیلی پیچیده تر است. توجه کنید که این یک نمونه کدِ خیلی ساده است و در برنامه های کاربردی، کد ها خیلی تو در تو تر از این هستند، که خوانایی برنامه را کمتر و کمتر می کند.
دیدگاهِ شما چیست؟
آیا آزانگر ها و koa
پیچیده هستند؟ آیا callback
ها و express
ناخوانا تر هستند؟
پیش از خواندنِ این نوشته، بخشِ یکم و بخشِ دومِ آن را بخوانید.
در بخشِ پیش دیدیم که چگونه می توانیم اجرای تابعِ آزانگر را کنترل کنیم. تابعی به نامِ 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
را هم خیلی ساده تر می کنند. در بخش های آینده با چند ابزارِ دیگر هم آشنا خواهید شد.
پیش از خواندنِ این نوشته، بخشِ نخستِ آن را بخوانید.
همان گونه که در بخشِ نخست دیدید، آزانگر ها به شما تواناییِ نگه داشتنِ اجرای تابع و سپس ادامه ی آن را می دهند. در اینجا می کوشم تا چگونگیِ کاربردِ آن ها برای کنترلِ روندِ اجرای برنامه را نشان دهم.
کنترلِ روندِ اجرا چیست؟
نمونه کدِ زیر را ببینید.
var fs = require('fs')
function readFile(path, done) {
fs.readFile(path, 'utf-8', done)
}
function readTwoFiles(file1, file2, done) {
readFile(file1, function (err, data1) {
readFile(file2, function (err, data2) {
done(null, [data1, data2])
})
})
}
readTwoFiles('md/file1.md', 'md/file2.md', function (err, res) {
console.log(res[0])
console.log(res[1])
})
در اینجا می خواهیم در تابعِ readTwoFiles
دو پرونده را بخوانیم. ولی می خواهیم نخست پرونده ی یک را بخوانیم و سپس پرونده ی دو را بخوانیم. این کار را با خواندنِ پرونده ی دوم درونِ callback
ِ پرونده ی یک انجام داده ایم. کنترلِ روندِ اجرا به همین گفته می شود. گاهی می خواهید کار ها پشتِ سرِ هم انجام شوند، گاهی می خواهید همزمان با هم انجام شوند و گاهی به روش های دیگر.
کنترلِ روندِ اجرا در جاوا اسکریپت، تا کنون با callback
ها انجام می شده است. اگر چه برنامه نویسان جاوا اسکریپت به آن ها خو گرفته اند و برنامه ها را می فهمند، ولی با آزانگر ها می توان کار ها را خیلی ساده تر کرد و کدِ ساده تر و فهمیدنی تری داشت.
به کار گیریِ آزانگر
اگر بخواهیم کدِ بالا را با آزانگر ها بنویسیم چه کار باید بکنیم؟ باید بتوانیم در زمانِ خواندنِ پرونده ی یک، اجرا را نگه داریم و داده های درونِ پرونده ی یک را به دست آوریم و سپس همین کار را برای پرونده ی دو انجام دهیم. ولی چگونه می توانیم داده های پرونده ی یک را به دست آوریم؟
در بخشِ نخستِ این نوشته دیدید که با فراخوانیِ gen.next()
می توانیم اجرا را ادامه دهیم. چیزی که در آنجا نگفتم این بود که می توانید چیزی را به next(...)
بفرستید و yield
با آن چیز جایگزین می شود. پس، اگر پس از نگه داشتنِ اجرای برنامه، داده ها را به دست آوریم، می توانیم آن را به درونِ تابع باز گردانیم.
اکنون می توانیم بخشی از کد را بنویسیم.
...
var data1 = yield readFile(file1)
var data2 = yield readFile(file2)
done(null, [data1, data2])
...
در اینجا نخست اجرای تابع نگه داشته شده تا داده های پرونده ی یک را به دست آوریم. سپس همین کار را برای پرونده ی دو انجام داده ایم. و سپس done
را فراخوانده ایم. چیزِ دیگری که دگرگون شده، readFile
است که این بار دیگر callback
نگرفته است. به آن خواهیم پرداخت. اگر به یاد داشته باشید، yield
را نمی توان درونِ تابع ها نوشت. باید درونِ آزانگر باشد. کدِ کامل را ببینید.
var fs = require('fs')
, run = require('./run')
function readFile(path) {
return function (done) {
fs.readFile(path, 'utf-8', done)
}
}
function readTwoFiles(file1, file2, done) {
run(function* () {
var data1 = yield readFile(file1)
var data2 = yield readFile(file2)
done(null, [data1, data2])
})
}
readTwoFiles('md/file1.md', 'md/file2.md', function (err, res) {
console.log(res[0])
console.log(res[1])
})
readFile
دیگر callback
نمی گیرد و تابعی را باز می گرداند که هر بار که این تابعِ بازگشتی فراخوانده شود، callback
را با داده های درونِ پرونده فرا می خواند. تابعِ run
کارِ کنترلِ روندِ اجرا را انجام می دهد. آزانگری که به آن فرستاده شود را اجرا می کند. کدِ زیر چگونگیِ نوشتنِ یک کنترل کننده ی روندِ اجرا را نشان می دهد.
module.exports = run
function run(fn) {
var gen = fn()
function next(err, res) {
var ret = gen.next(res)
if (ret.done) return
ret.value(next)
}
next()
}
به همین سادگی و کوتاهی. تابعِ run
یک تابعِ آزانگر می گیرد. آزانگرِ آن را می سازد. یک تابعِ درونی به نامِ next
تعریف می کند که کارِ اجرا را انجام می دهد. سپس آن را فرا می خواند. در درونِ next
آزانگر اجرا می شود. اگر کارِ آزانگر پایان یافته باشد که هیچ. اگر نه، باید یک تابع را آزانیده باشد (value
باید یک تابع باشد). آن را فرا می خواند و خود را callback
ِ آن می کند. این کار را ادامه می دهد و هر بار که به callback
چیزی باز گشته باشد (res
)، آن را به آزانگر می دهد تا کار را ادامه دهد.
کنترلِ خطا
شاید با خود بگویید که خیلی پیچیده شد و کد چندان هم بهتر نشده. نمونه های بالا خیلی ساده شده بودند تا بتوانم کنترلِ روند را توضیح بدهم. ولی در کد هایی که در برنامه ها می نویسیم، کار به این سادگی نیست. چیزِ مهمی که در این جا ندیدیم، گرفتنِ خطا ها بود. گرفتنِ خطا ها در جاوا اسکریپت و برنامه های Node.js
کارِ بسیار دشواری است. در کدِ زیر، خطاهای نمونه کدِ با callback
کنترل شده اند.
var fs = require('fs')
function readFile(path, done) {
fs.readFile(path, 'utf-8', done)
}
function readTwoFiles(file1, file2, done) {
readFile(file1, function (err, data1) {
if (err) return done(err)
readFile(file2, function(err, data2) {
if (err) return done(err)
done(null, [data1, data2])
})
})
}
readTwoFiles('md/file1.md', 'md/file2.md', function (err, res) {
if (err) return console.error(err.message)
console.log(res[0])
console.log(res[1])
})
می بینید که درونِ هر callback
یک بار بررسی کرده ایم که آیا خطایی پیش آمده و یا نه. اگر خطا پیش آمده، done
را با آن فرا می خوانیم. این کار را خیلی دشوار می کند. برنامه هایی که می نویسیم، همیشه خیلی پیچیده تر از این نمونه کدِ ساده هستند. زبان های برنامه نویسی، try/catch
را برای ساده سازیِ همین مشکل آورده اند ولی در callback
های جاوا اسکریپت، نمی توانیم آن ها را به کار بریم. چون کتاب خانه های درونیِ Node.js
و دیگر کتاب خانه ها، تابعِ callback
را درونِ یک try/catch
فرا نمی خوانند.
اکنون بیایید کنترلِ خطا ها را با آزانگر ها انجام دهیم. چیزِ دیگری که در بخشِ نخست نگفتم، این است که به جای فراخوانیِ next()
می توانید throw()
را فرا بخوانید تا یک خطا در جایی از کد که yield
بوده، throw
شود. کدِ زیر را ببینید.
var fs = require('fs')
, run = require('./run')
function readFile(path) {
return function (done) {
fs.readFile(path, 'utf-8', done)
}
}
function readTwoFiles(file1, file2, done) {
run(function* () {
try {
var data1 = yield readFile(file1)
var data2 = yield readFile(file2)
done(null, [data1, data2])
} catch (err) {
done(err)
}
})
}
readTwoFiles('md/file1.md', 'md/file2.md', function (err, res) {
if (err) return console.error(err.message)
console.log(res[0])
console.log(res[1])
})
در اینجا خیلی ساده یک try/catch
گذاشته ایم تا خطا ها را بگیریم. خیلی ساده تر از کدِ نخست است. کدِ run
را هم ببینید.
module.exports = run
function run(fn) {
var gen = fn()
function next(err, res) {
if (err) return gen.throw(err)
var ret = gen.next(res)
if (ret.done) return
ret.value(next)
}
next()
}
تنها یک خط افزوده شده که اگر خطایی رخ داده باشد، throw(err)
خطا را در درونِ آزانگر throw
می کند.
چه به دست آوردیم؟
تا اینجا دیدید که آزانگر ها در کنترلِ روندِ اجرا به ما خیلی کمک می کنند. این که می توانیم در میانه ی اجرا تابع را نگه داریم، کاری را انجام دهیم، سپس کار را از همان جا ادامه دهیم یا اینکه خطایی به جای آن throw
کنیم، بسیار بسیار ویژگیِ توانمندی است.
کدی که با آزانگر ها نوشته شود می تواند هم ساده تر باشد، هم روندِ برنامه را بهتر نشان دهد، هم کنترلِ خطای خیلی بهتری داشته باشد. در بخش آینده، با یک ابزار بسیار پیشرفته برای کنترلِ روندِ اجرا آشنا خواهید شد.
تازه ترین نسخه ی جاوا اسکریپت (ES6) یک ویژگی به نامِ «آزانگر» (Generator) دارد که در این نوشته می کوشم شما را با آن آشنا کنم.
آزانگر؟
شاید بپرسید که آزانگر چیست و آن را از کجا در آورده ام و چرا نمی گویم «تولید کننده» یا «سازنده» یا «بوجود آورنده» یا «آفریننده» یا «generate کننده» یا چیز های دیگر.
آزانگر را از واژه نامه ی ریشه شناختی اختر شناسی-اختر فیزیک حیدری ملایری آورده ام که واژه نامه ی بسیار دقیقی است و این زمانِ خوبی برای آوردنِ این واژه است، چون این ویژگی تازه به زبان افزوده شده و می توان در همین آغازِ کار واژه ی درست و بجایی را برای آن برگزید، پیش از آن که واژه های نادرستِ دیگر به جای آن به کار برده شوند.
آزانگر چیست؟
آزانگر تابعی است که می توان در میانه ی اجرای آن از آن بیرون آمد و سپس دوباره از همان جا اجرا را ادامه داد.
ولی این توضیح ارزش آن را نشان نمی دهد. با یک نمونه کد بهتر می شود آن را فهمید.
نمونه کدِ ساده
کدِ زیر را ببینید تا آن را با هم بررسی کنیم.
function* sample() {
yield 1
yield 2
return 3
}
var gen = sample()
console.log(gen.next())
console.log(gen.next())
console.log(gen.next())
// output:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: true }
نخستین چیزی که تازه است، function*
است که یک آزانگر را تعریف می کند. در همین آزانگر، yield
را نیز می بینید که تنها می توان آن را در آزانگر ها نوشت. در خطِ ۱ تا ۵ آزانگرِ sample
را تعریف کرده ایم. در خطِ ۷ آن را فراخوانده ایم که object ای باز می گرداند که آن را gen
نامیده ایم. سپس ۳ بار next()
را روی آن فراخوانی کرده ایم.
در بارِ نخست که next()
فراخوانده شده، آزانگرِ sample
اجرا شده و تا yield 1
پیش می رود. سپس 1
باز می گردد و تابع در همان جا می ایستد. با فراخوانیِ دوباره ی next()
تابع کار را ادامه می دهد تا به yield 2
می رسد. سپس همان جا می ایستد و 2
را باز می گرداند. در بارِ سوم اجرای تابع ادامه می یابد تا به return 3
می رسد. در اینجا کارِ آزانگر پایان می یابد و 3
را باز می گرداند.
نتیجه در پایان آورده شده است. هر بار که next
فراخوانده شده، یک object باز گشته که ۲ کلید دارد: value
که مقدار باز گشته از آزانگر را در خود دارد و done
که می گوید آیا کارِ آزانگر پایان یافته یا نه.
ویرایش: تابعی که با function*
تعریف می شود، تابعِ آزانگر یا GeneratorFunction
نامیده می شود و object ای که با فراخوانیِ آن باز می گردد یک Generator Object
است.
function* genFun() {}
console.log(genFun.constructor.name) // "GeneratorFunction"
var genObj = genFun()
console.log(genObj.toString()) // [object Generator]
چگونه آزانگر ها را اجرا کنید
Firefox در نسخه ی ۲۶ و پس از آن، از آزانگر ها پشتیبانی می کند. می توانید نمونه کدِ بالا را در console ِ آن بنویسید تا نتیجه را ببینید.
Chrome در نسخه ی ۲۹ و پس از آن، از آزانگر ها پشتیبانی می کند ولی باید یک flag را فعال کنید تا بتوانید آزانگر ها را اجرا کنید. برای این کار، به about:flags
یا chrome://flags/
بروید و Enable Experimental JavaScript
را فعال کنید. سپس Chrome را یک بار ببندید و دوباره باز کنید. اکنون می توانید نمونه کد را اجرا کنید.
Node.js هم در آخرین نسخه ی خود که هنوز پایدار نشده (0.11+
) از آزانگر ها پشتیبانی می کند ولی برای فعال کردنِ آن ها باید node
را با --harmony-generators
یا --harmony
اجرا کنید:
$ node --version
v0.11.12
$ node --harmony-generators sample.js
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: true }
نمونه کدِ پیشرفته تر
یک نمونه کدِ بهتر و کاربردی تر در زیر آمده است.
function* fibonacci() {
var fn1 = 1
, fn2 = 1
, current
while (true) {
current = fn2
fn2 = fn1
fn1 = fn1 + current
yield current
}
}
var sequence = fibonacci()
function logNext() {
console.log(sequence.next().value)
setTimeout(logNext, 1000)
}
logNext()
این آزانگرِ بی پایانِ فیبوناچی است که هر بار یک عدد را در دنباله ی فیبوناچی می آزاند. این بار آزانگر در یک چرخه ی بی پایان است. برای اجرای آن هم هر یک ثانیه یک بار next
را روی آن فراخوانده ایم.
همین؟
شاید با خود بگوییم که جالب بود ولی نیازی به دنباله ی فیبوناچی یا این جور چیز ها نداریم که بخواهیم آزانگر ها را به کار بریم. آزانگر ها خیلی بیشتر از این اند و جاوا اسکریپت را در بسیاری از زمینه ها دگرگون خواهند کرد. توانایی نگه داشتن یک تابع در میانه ی اجرا و سپس ادامه ی آن، توانایی بسیار پر کاربردی است. این نوشته تنها برای آشنایی بود. در بخش دوم کمی با کاربرد های پیشرفته ترِ آزانگر ها آشنا خواهیم شد.