بهرنگ نوروزی نیا درباره بایگانی

گاهشماریِ جلالی

بیشترِ نرم افزار هایی که فارسی هستند، نیاز دارند تا زمان را به تاریخِ ایران نشان دهند. برای این کار چندین ابزار و کتابخانه به زبان های گوناگون نوشته شده است. سالِ گذشته، زمانی که روی یک نرم افزار کار می کردم، خیلی به دنبالِ یک کتابخانه ی تاریخ گشتم ولی هیچ کدام نیاز هایم را براورده نکرد. بیشترِ آن ها الگوریتمِ نادرستی داشتند. برخی تنها در یک بازه ی زمانیِ کوتاه که کمتر از ۱۰۰ سال بود کار می کردند. برخی مشکلِ پروانه (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 انجام دهیم.

کاربردِ koa برای نوشتنِ وب سرور ها با آزانگر (Generator) در جاوا اسکریپت - بخشِ چهار

پیش از خواندنِ این نوشته، بخشِ یکم، بخشِ دومِ و بخشِ سومِ آن را بخوانید.

در بخشِ پیش با 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 ناخوانا تر هستند؟

کاربردِ 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 را هم خیلی ساده تر می کنند. در بخش های آینده با چند ابزارِ دیگر هم آشنا خواهید شد.

کاربردِ آزانگر (Generator) در جاوا اسکریپت برای کنترلِ روندِ اجرا - بخشِ دو

پیش از خواندنِ این نوشته، بخشِ نخستِ آن را بخوانید.

همان گونه که در بخشِ نخست دیدید، آزانگر ها به شما تواناییِ نگه داشتنِ اجرای تابع و سپس ادامه ی آن را می دهند. در اینجا می کوشم تا چگونگیِ کاربردِ آن ها برای کنترلِ روندِ اجرای برنامه را نشان دهم.

کنترلِ روندِ اجرا چیست؟

نمونه کدِ زیر را ببینید.

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 کنیم، بسیار بسیار ویژگیِ توانمندی است.

کدی که با آزانگر ها نوشته شود می تواند هم ساده تر باشد، هم روندِ برنامه را بهتر نشان دهد، هم کنترلِ خطای خیلی بهتری داشته باشد. در بخش آینده، با یک ابزار بسیار پیشرفته برای کنترلِ روندِ اجرا آشنا خواهید شد.

آشنایی با آزانگر (Generator) در جاوا اسکریپت - بخشِ یک

تازه ترین نسخه ی جاوا اسکریپت (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 را روی آن فراخوانده ایم.

همین؟

شاید با خود بگوییم که جالب بود ولی نیازی به دنباله ی فیبوناچی یا این جور چیز ها نداریم که بخواهیم آزانگر ها را به کار بریم. آزانگر ها خیلی بیشتر از این اند و جاوا اسکریپت را در بسیاری از زمینه ها دگرگون خواهند کرد. توانایی نگه داشتن یک تابع در میانه ی اجرا و سپس ادامه ی آن، توانایی بسیار پر کاربردی است. این نوشته تنها برای آشنایی بود. در بخش دوم کمی با کاربرد های پیشرفته ترِ آزانگر ها آشنا خواهیم شد.