Dart: การเรียกใช้ function แบบ dynamic ด้วยคำสั่ง Function.apply()

ในการเรียกใช้ฟังก์ชั่นที่ผู้ใช้งานเขียนขึ้น จำเป็นต้องระบุตัวแปรที่จะส่งให้ในโปรแกรม แต่หากมีความจำเป็นต้องเขียนฟังก์ชั่นที่อาจรับค่าจากข้อมูลภายนอก ที่ไม่สามารถระบุใจขณะเขียนโปรแกรมได้ ตัว Dart มีคำสั่ง Function.apply() ซึ่งเป็น static method ของ Function class มาช่วยในงานแบบนี้

การส่งตัวแปรให้ฟังก์ชั่น

Dart รองรับการส่งตัวแปรทั้งแบบตามลำดับตำแหน่ง(positional parameters) และแบบใช้ชื่อตัวแปร(named parameters) ดูรายละเอียดเพิ่มเติมได้ที่ Functions | Dart

num plusOne(num input) {
  return input + 1;
}

num plusTwo({required input}) {
  return input + 2;
}

num addThem(num input1, {required input2}) {
  return input1 + input2;
}

void main() {
  print(plusOne(1)); // output → 2
  print(plusTwo(input: 1)); // output → 3
  print(addThem(1, input2: 2)); // output → 3
}

จากตัวอย่างจะเห็นว่า การเรียกใช้ฟังก์ชั่น ต้องเขียนระบุลงไปเลยว่าจะเรียกใช้ฟังก์ชั่นโดยส่งผ่านค่าอะไรไปให้บ้าง

การใช้งาน Function.apply()

สำหรับคนที่เคยเขียนมาหลายภาษาโปรแกรม การใช้คำสั่ง Function.apply() คงเป็นเรื่องธรรมดา ถึงแม้จะไม่ได้ค่อยได้ใช้ก็ตาม รูปแบบวิธีการใช้งานมีดังนี้

external static apply(
  Function function, 
  List<dynamic>? positionalArguments,
  [Map<Symbol, dynamic>? namedArguments]
);

จากตัวอย่างแรก หากเขียนด้วย Function.apply() สามารถทำได้ดังนี้

num plusOne(num input) {
  return input + 1;
}

num plusTwo({required input}) {
  return input + 2;
}

num addThem(num input1, {required input2}) {
  return input1 + input2;
}

void main() {
  print(Function.apply(plusOne, [1])); // output → 2
  print(Function.apply(plusTwo, [], {#input: 1})); // output → 3
  print(Function.apply(addThem, [1], {#input2: 2})); // output → 3

  print(Function.apply(plusTwo, [], {Symbol('input'): 1})); // output → 3
  print(Function.apply(addThem, [1], {Symbol('input2'): 2})); // output → 3
}

มาถึงตรงนี้ ก็อาจมีคนสงสัยว่า ในเมื่อฟังก์ชั่นใน Dart มันสามารถรับ List เป็น parameters ได้อยู่แล้ว จะมาเขียนแบบนี้ทำไม ถ้าตอบแบบกำปั้นทุบดิน ก็เพราะมันเข้าใจการทำงานง่ายกว่า เช่น หากเขียนฟังก์ชั่นเพื่อรับค่าที่เป็น optional มันสามารถเขียนลงไปตอนประกาศฟังก์ชั่นได้เลย

String info1(String name, [String nationality = 'Thai', int age = 0]) {
  return "Name: $name\nNationality:$nationality\nAge:$age";
}

String info2(List<dynamic> info) {
  if (info.isEmpty) {
    throw Exception('require name');
  }

  return "Name: ${info[0]}\nNationality:${info.elementAtOrNull(1) ?? 'Thai'}\nAge:${info.elementAtOrNull(2) ?? 0}";
}

void main() {
  print(Function.apply(info1, ['Somchai N.']));
  print(info2(['Somchai N.']));
}

จากตัวอย่าง ทั้ง info1() และ info2() ต่างก็ทำงานเหมือนกัน แต่การประกาศค่า parameters นั้น การที่ระบุชื่อตัวแปร ประเภทตัวแปร รวมถึงค่า default ทำให้อ่านง่ายกว่าที่ส่งมาเป็น List ใน info2

งานในชีวิตจริง

การเรียกแบบ dynamic มันช่วยให้ลดการเขียนโปรแกรมเพื่อเอาค่าที่ได้จากข้อมูลภายนอก มาจัดรูปแบบเพื่อนำส่งให้ฟังก์ชั่นประมวลผลได้ เช่น หากมี Text file ที่เป็นจากตัววัดอุณหภูมิ ที่จะเก็บในรูปแบบต่อไปนี้

date time degree rh
2024-01-01 12:00 15.5 30
2024-01-01 13:00 15.5 45
2024-01-01 14:00
2024-01-01 15:00 14 60

โดยมันจะมีค่าว่างหากตัวข้อมูลวัดไม่ได้ ผู้เขียนก็เขียนฟังก์ชั่นเพื่อเอาข้อมูลดังกล่าวมาประมวลผลได้ดังนี้

import 'dart:io';

void getTempRh([String? date, String? time, String? degree, String? rh]) {
  // process data
  print("$date $time $degree $rh");
}

void main() {
  // ** example: read data from file **
  // File textData = File('path/to/datafile.txt');
  // List<String> lines = textData.readAsLinesSync();

  // ** example: use inline demo data **
  String textData = """
2024-01-01 12:00 15.5 30
2024-01-01 13:00 15.5 45
2024-01-01 14:00
2024-01-01 15:00 14 60
""";
  List<String> lines = textData.split("\n");

  for (String line in lines) {
    List<String> param = line.trim().split(' ');
    Function.apply(getTempRh, param);
  }
}

ผลที่ได้

2024-01-01 12:00 15.5 30
2024-01-01 13:00 15.5 45
2024-01-01 14:00 null null
2024-01-01 15:00 14 60
null null null

อีกตัวอย่าง หากได้ข้อมูลจากการอ่านค่าเป็น JSON ในงานที่อ่านข้อมูลมาจาก Server ก็สามารถส่งข้อมูลแบบ dynamic call ได้เช่นเดียวกัน

import "dart:convert";

void getTempRh({String? date, String? time, String? degree, String? rh}) {
  // process data
  print("$date $time $degree $rh");
}

void main() {  
  String jsonText = """[
{"date":"2024-01-01","time":"15:00","degree":"15.5","rh":"40"},
{"date":"2024-01-01","time":"16:00","degree":"15","rh":"45"}
]""";

  var data = jsonDecode(jsonText);
  for (Map line in data) {
    Function.apply(getTempRh, [], line.map((k, v) => MapEntry(Symbol(k), v)));
  }
}

ผลที่ได้

2024-01-01 15:00 15.5 40
2024-01-01 16:00 15 45

จากตัวอย่าง JSON เนื่องจากตัว named arguments ต้องส่งในรูปแบบ Map<Symbol, dynamic> จึงจำเป็นต้องแปลงตัว key ที่เป็น String ให้เป็น Symbol เสียก่อน โดยเรียกใช้คำสั่ง .map() เพื่อแปลงค่า key ของทุก elements

สามารถดูวิธีการใช้งาน Map เบื้องต้นได้ที่ note ตัวนี้

ข้อเสียของการใช้ dynamic call

เนื่องจากมันเป็นการทำงานขณะ runtime ทำให้ไม่สามารถตรวจสอบได้ว่าตัวแปรที่ส่งให้ประเภทถูกต้องตามที่ต้องการหรือไม่ อาจทำให้เกิด Exception error ได้ และหากใช้งานไม่วางแผนให้ดีพอจะกลายเป็น bugs เพราะมีช่องโหว่ที่ตัว compiler ไม่สามารถช่วยตรวจสอบให้ได้ ดังนั้นถ้ามันเป็นงานที่ไม่ซับซ้อนมาก ผู้เขียนโปรแกรมสามารถควบคุมดูแลความถูกต้องของข้อมูลที่จะใช้ประมวลผลได้ ก็ถือว่าช่วยผ่อนแรงในการเขียนโปรแกรมไปได้เยอะพอสมควรกับการเรียกใช้คำสั่ง Function.apply() โปรดใช้งานด้วยความระมัดระวัง