ผมค่อนข้างมั่นใจว่า React Developer รวมถึงคนที่เขียน JavaScript / TypeScript จำนวนไม่น้อยที่เคยเขียน code แบบ Point-free กันมาบ้างแล้ว ไม่ว่าจะรู้ตัวหรือไม่รู้ก็ตามว่ามันมีชื่อเรียกแบบนี้
เรื่องที่เขียนใน Blog post นี้ ค่อนข้างเกี่ยวข้องกับ Functional Programming (FP) อยู่ไม่น้อย และปัจจุบันมีหลายภาษาได้เอาแนวคิดของ FP มาใส่ในภาษาตนเองกันแทบทุกภาษาแล้ว
โดยบทความนี้ยกตัวอย่างเป็นภาษา TypeScript ที่ชาว web dev ส่วนใหญ่คุ้นเคยฮะ
Unary & Point-free ไม่ขึ้นอยู่กับ Programming Language ใดๆ นั่นหมายความว่าเราจะนำความรู้เรื่องนี้ไปใช้กับภาษาอะไรก็ได้ (ขอแค่มัน Support)
และเราจะได้เห็นคำว่า Argument และ Parameter ถูกใช้สลับกันใน blog post นี้อยู่บ่อยๆ ซึ่งทั้งสองคำนี้มันก็คือสิ่งเดียวกันนั่นแหละ โดยมันแตกต่างกันที่…
- Parameter ใช้กับการประกาศฟังก์ชัน เช่น
function add10(x: number) { return x + 10 }
หมายถึง เรามี parameterx
- Argument ใช้กับตอนเรียกใช้ฟังก์ชัน และส่ง value ต่างๆ ไปให้ฟังก์ชันอ่านและ process ให้เรา เช่น
add10(20)
เราส่ง value20
ไปเป็น argument ของฟังก์ชันadd10
(อ่านว่า `add10` of 20)
Table of Content
จะอ่านทั้งหมด หรือจิ้มๆ หัวข้อไปอ่านก็ได้
Point-free style
เป็น style การเขียน code รูปแบบหนึ่งซึ่งซึ่งเขียน function ให้มัน general มากพอที่จะนำกลับมาใช้ซ้ำได้ โดยที่ไม่ต้องระบุ arguments (points) ใดๆ ณ จุดที่ใช้งาน function นั้นๆ
(ผมว่าผมแปลไม่ดีเท่าไรนะ เผื่อใครอยากอ่านต้นฉบับลองไปอ่านต่อ >>> ที่นี่ <<< ได้ฮะ)
นอกจากนี้มันยังมีประโยชน์ในการนำ function ที่เขียนด้วย point-free style ไปใช้ทำ composition function ได้ยืดหยุ่นกว่า (ผมแปลมาอีกที และยังไม่ค่อยเข้าใจเท่าไร จึงไม่ขอยกตัวอย่างในเรื่องประโยชน์ของการนำ point-free ไป compose นะ)
โดยเท่าที่พอทราบมา (นิดหน่อย) Point-free style เป็นแนวคิดที่เริ่มต้นมาจากหนึ่งในศาสตร์ของ Abstract Math และใช้กันโดยทั่วไปในภาษาคอมพิวเตอร์ฝั่งที่เป็น Functional Programming Paradigm (อย่างเช่น Lisp, ภาษาตระกูล ML, Haskell)
Example of Point-free:
ผมว่าเราน่าจะคุ้นเคยกับ React JS ที่สามารถทำแบบนี้
// event handler function
function handleOpenSomeModal(e: MouseEvent<HTMLButtonElement>) {
e.stopPropagation()
setOpenedSomeModal(true)
}
// jsx
<button onClick={handleOpenSomeModal}>
Create Something
</button>
หรือเราคุ้นเคยกับท่านี้ใน Express JS
// request handler function
function createNewThing(req: Request, res: Response, next: NextFunction) {
// Do something
res.json({ createdThing: {...} })
}
server.post('/things', createNewThing)
(ไม่แน่ใจนะว่า express เวอร์ชันปัจจุบันยังใช้ท่านี้รึเปล่า ไม่ได้เขียนมานานมากแล้ว)
เราจะเห็นได้ว่า ทั้งสองตัวอย่างข้างต้นมีจุดร่วมเหมือนๆ กันก็คือ
1) การที่เราส่ง handleOpenSomeModal
ไปเป็น prop onClick
และ 2) เราส่ง createNewThing
function ไปเป็น argument ที่ 2 ของ Express.post
ได้โดยที่ไม่ได้ส่ง argument ใดๆ ไปพร้อมกับ handler function ทั้งสองเลย
สิ่งนี้เราเรียกมันว่า point-free ตามนิยามที่ผมแปลไว้ข้างบนนั่นเอง
คนที่ไม่รู้ว่าเขียนท่าข้างบนได้อาจจะเขียนแบบนี้
onClick={(e) => handleOpenSomeModal(e)}
และserver.post(‘/things’, (req, res, next) => createNewThing(req, res, next))
เราคงสังเกตเห็นเหมือนกันใช่หรือไม่ว่า มันคือ points หรือ arguments ที่แต่ละตัวมี type เดียวกันในแต่ละลำดับ
อีกตัวอย่างหนึ่งที่เราอาจเคยใช้มันมาอยู่บ้าง นั่นคือ…
Mapper function ที่เป็น parameter ของ Array.prototype.map
อย่างที่เราทราบกันดีอยู่แล้วว่า method map
ที่มีให้เรียกใช้จาก instance ของ Array โดยที่ map
คือ method ที่รับ argument เป็น callback function เพื่อ map ข้อมูลจาก type หนึ่งไปเป็นอีก type หนึ่ง
ในที่นี้ผมขอใช้คำว่า map จาก type A
ไป type B
(หรือ map: A → B
)
ดังนั้นหน้าตาของ function signature ที่เราต้องส่งไปเป็น callback ของ Array.prototype.map จะมีหน้าตาประมาณนี้
type MapperFn<A, B> = (x: A, index?: number, xs?: A[]) => B
จะเห็นได้ว่าผมเขียน
MapperFn
type ให้มี function signature
ล้อกับ mapper function ของ methodmap
ตรงอย่างตัวเลยนะโดยที่
x
คือ current item ซึ่งเป็น required parameter
ในขณะที่index
และxs
(x
เติมs
คือ x หลายตัว) เป็น optional parameters หมายความว่าเราอาจจะส่ง argumentindex
และ/หรือxs
หรือจะไม่ส่งเลยก็ได้
Q: แล้วมัน Point-free ยังไง?
A: เอาล่ะ เย็นไว้ ผมกำลังจะเล่าต่อฮะ
point-free มันมีประโยชน์แบบนี้
สมมติเรามีโจทย์ว่าเราอยากได้ function เพื่อติ๊ก Completed ให้กับ Todo List ทั้งหมดในคราวเดียว
เรากำหนด function completeTodo
โดยมี signature ของ function คือ ITodo -> ITodo
ดัง code snippet ข้างล่างนี้
interface ITodo {
title: string;
completed: boolean;
}
function completeTodo(todo: ITodo): ITodo {
return { ...todo, completed: true }
}
จากนั้นก็นำ function completeTodo
ไปใช้
function completeAllTodos(todos: ITodo[]) {
return todos.map(completeTodo)
}
จะเห็นได้ว่า เราใช้ completeTodo
เป็น mapper function ที่ map จาก ITodo
ไป ITodo
นั่นเอง (การ map A -> B
สามารถใช้ type เดียวกันได้ ไม่มีกฎข้อไหนห้ามฮะ)
Q: ก็งงอยู่ดีอะ แล้วมัน point-free ตรงไหน?
A: ก็ point-free มันช่วยให้เรากำหนด function ได้โดยไม่ต้องระบุ arguments ใดๆ ยังไงล้าาา
Q: ถ้า point-free ไม่กำหนด argument แบบนั้น แล้ว point ไม่ฟรี เป็นยังไง
A: เดี๋ยวเล่าเป็น code แล้วกัน…แบบนี้ไงฮะะ
todos.map((todo) => completeTodo(todo))
เราอาจสังเกตเห็นว่ามี (todo) => completeTodo(todo)
ซึ่งข้างหน้า และหลังของ completeTodo
มีจำนวนสมาชิก arguments เท่ากัน
และทั้งคู่มี type ตรงกัน (ITodo -> ITodo
)
ทำให้เราสามารถละ arguments (points) พวกนั้นได้
และเหลือแค่ชื่อฟังก์ชัน completeTodo
เพื่อส่งไปเป็น argument ของ Array.prototype.map ได้เลยนั่นเอง
Rules of Point-free
ต่อไปนี้เป็นกฎที่จะทำให้เราเขียน point-free style ได้
(ซึ่งจริงๆ ไม่น่าเรียกว่ากฎได้หรอกนะ เพราะผมสรุปเองจากประสบการณ์ที่เคยใช้มันมาเท่านั้นครับ)
- จำนวนสมาชิกของ function ที่จะส่งไปเป็น argument ของ function ใดๆ มีจำนวนเท่ากัน
- ต่อเนื่องจากกฎข้อแรก แล้วแต่ละลำดับของ argument ที่จะทำ point-free ต้องมี type ตรงกันด้วย
Note:
Rule 1: ตัวอย่างเช่นข้างหน้าและหลังของcompleteTodo
มีแค่todo
ตัวเดียวเท่ากัน (จริงๆ มันมีรายละเอียดมากกว่านี้ โดยใน blog post นี้อาจจะเล่าได้ไม่หมดนะ)Rule 2: ถ้า type ข้างหน้าและหลังไม่ตรงกันก็ไม่สามารถใช้เป็น point-free ได้
ซึ่งผมก็นึกไม่ออกนะ ว่ามันทำให้ตัวหน้าและตัวหลังมี type ไม่ตรงกันได้ด้วยเหรอ?แต่ถ้าจำไม่ผิดเหมือนเคยเจอเพื่อนหรือน้องสักคนที่(อาจจะ)ยังไม่เข้าใจเรื่อง point-free เล่นท่าพิเรนทร์จนเจอเคสนี้อยู่อะครับ 555 (นั่นคือเหตุผลว่าทำไมผมเขียน Rule 2 ขึ้นมา)
Q: แล้วจะสร้าง function completeTodo
แล้วระบุ type แถมต้องมาตั้งชื่อ function ให้มันยุ่งยากเยอะแยะตาแป๊ะไก๋ทำไม ทำไมไม่ทำแบบนี้ไปเลย?
todos.map((todo) => {
return { ...todo, completed: true }
})
// OR
todos.map(todo => ({ ...todo, completed: true }))
ควรจะเป็นแบบข้างบนสิ สั้นๆ ง่ายๆ บรรทัดเดียว/ไม่กี่บรรทัดเอง
A: มันมีประโยชน์หลายๆ อย่างนะ ผมขอตอบสักสองตัวอย่างเท่าที่นึกออกแล้วกัน
Note:
การตอบตรงนี้ถือว่านอกเรื่อง point-free และตอบยาว(มาก)หน่อยนึง
สำหรับคนไม่อยากอ่านคำตอบยาวๆ นี้ต่อ ผมก็มีสรุปสั้นๆ ให้ดังนี้
1. ช่วยให้เราไล่หาตำแหน่งที่เกิด error ได้ง่ายขึ้น
2. นำไปใช้ซ้ำต่อได้ (reusable function)ถ้าไม่อ่านส่วนนี้ต่อ ให้ข้ามไปอ่านส่วนถัดไปเลยได้เลย
>>> ปัญหาของ Point-free <<< (จิ้มแรงๆ)
1.
ในกรณีที่ function เล็กๆ คงไม่เป็นไรหรอกนะ
แต่ถ้า function มันซับซ้อน เขียนเยอะๆ ยาวๆ แล้วเกิด run-time error
จาก mapper function ที่เราส่งไปใน Array.prototype.map ล่ะ?
การ debug จะยากขึ้นประมาณนึงเลยนะ
เพราะ call stack trace จะบอกว่า callback function ของเรามันคือ Anonymous Function ซึ่งเกิดจากการเขียน function ที่ไม่ได้ตั้งชื่อ
แล้วจะดีกว่าไหมนะ? ถ้าเราตั้งชื่อเป็น completeTodo
หรืออะไรก็ได้ที่เราเห็นควร
เมื่อเกิด error ก็ให้มันฟ้องมาเลยว่า error เกิดขึ้นที่ function completeTodo
ช่วยให้เราไม่ต้องเสียเวลาไล่หา ว่ามันเกิดขึ้นที่ไหนนานๆ (ก็ชื่อ function ชัดเจนขนาดนั้น)
ต่อไปนี้เป็นตัวอย่าง Error ที่เกิดขึ้นใน Annonymous function และ Named Function
Without function name (Annonymous Function):
With function name which is completeTodo
(Named Function)
เห็นใช่มั้ยฮะ ว่าแบบที่ 2 มีบอกชื่อ function ที่เกิด error ใน call stack trace ด้วย
ทำให้เราตามไปแก้ที่ function นั้นๆ ได้แทบจะทันทีเลย
∴ อยากได้แบบไหน ท่านเลือกได้เองฮะะ
ประโยชน์ข้อแรกจบไปละมาต่ออีกสักข้อนึงแล้วกัน
2.
บางครั้งเราจะได้เขียน callback (mapper) function รูปแบบเดียวกันนี้อีก
มันอาจจะดีกว่าก็ได้นะ ถ้าเราประกาศ function ไปตั้งแต่แรกเลย
เรื่องของ Point-free ที่ผมจะเขียนถึงก็มีอยู่เท่านี้ครับ
มาต่อกันที่ Unary Function กัน…
Unary Function
Unary มาจากคำว่า uni + ary โดยที่…
- Uni หมายถึง หนึ่งเดียว
- Ary มาจากคำว่า Arity มาจากภาษา latin ซึ่งในทางภาษาศาสตร์แปลว่า ความจุ (valency)
ดังนั้น Unary Function ก็คือฟังก์ชันที่รับ argument (parameter) 1 ตัวเท่านั้น
Good to know:
นอกจาก Unary (1) แล้ว
เรายังมี Nullary (0), Binary (2), Ternary (3), Quaternary (4), …, N-ary (n)
Example of Unary Function
function add1(x: number) {
return x + 1
}
function add(y: number) {
return function forX(x: number) {
return x + y
}
}
function map<A, B>(mapper: MapperFn<A, B>) {
return function forArray(arr: A[]): B[]{
return arr.map(mapper)
}
}
type PredicateFn<T> = (x: T): boolean
function filter<A>(predicate: PredicateFn<A>) {
return function forArray(arr: A[]): A[] {
return arr.filter(predicate)
}
}
จากภาพข้างบน
เราจะเห็นได้ว่าฟังก์ชัน add1
รับ x
เป็น parameter แค่ตัวเดียว
เราจะเห็นได้ว่าฟังก์ชัน add
รับ y
เป็น parameter แค่ตัวเดียว
เราจะเห็นได้ว่าฟังก์ชัน map
รับฟังก์ชัน mapper
เป็น parameter เพียงตัวเดียว
และเราจะเห็นได้ว่าฟังก์ชัน filter
รับฟังก์ชัน predicate
เป็น parameter เพียงตัวเดียวเช่นกัน
และเราจะสังเกตได้ว่าฟังก์ชัน add
, map
, และ filter
ต่างก็ return ฟังก์ชันที่รับ parameter ตัวเดียวเช่นกัน (return unary function) ซึ่งท่านี้ใน Lambda Calculus และฝั่ง FP เหมือนจะมีชื่อเรียกว่า Currying ซึ่งผมจะไม่เขียนถึงมันใน post นี้
เราจะมี Unary Function ไปเพื่ออะไร ? (แลดูยุ่งยากจัง)
- ใช้แก้ไขปัญหาของ Point-free ในภาษา JS / TS และอีกหลายๆ ภาษาในฝั่ง Imperative Programming เราสามารถ
- ช่วยให้เราทำ Function Composition (compose) ได้ง่าย และ powerful
ใช่ครับมันคือเรื่อง compose function ที่หลายๆ คนเคยได้เรียนกันในวิชาคณิตศาสตร์ ม.4 นั่นแหละ
และเรื่อง function composition ผมจะไม่เขียนใน post นี้ด้วย - ฯลฯ
ปัญหาของ Point-free
ที่จริงแล้ว ปัญหาไม่ได้อยู่ที่ Point-free หรอกนะ
แต่ปัญหามันอยู่ที่ function ในภาษา programming ต่างๆ
ดันอนุญาตให้ function ใดๆ รับ parameter ได้มากกว่า 1 ตัวมากกว่า
จะยกตัวอย่างได้ ผมคงต้องดึง method map
ของ Array
กลับมา re-run อีกที
เรามี mapper ที่เป็น callback function ของ map
ซึ่งมี function signature แบบนี้
type MapperFn<A, B> = (x: A, index?: number, xs?: A[]) => B
เราเห็นแล้วใช่มั้ยว่ามันรับ arguments ได้ตั้งแต่ 1–3 ตัว
ซึ่ง function ในทางคณิตศาสตร์ และโลกของ FP เขาไม่ทำกันแบบนั้น
และด้วยความที่ argument ตัวที่ 2 ของ mapper function ดันเป็น index ที่จะ dynamic ไปตามลำดับที่ n-1
อยู่เสมอ ทำให้การนำ point-free ไปใช้กับบาง function เกิดปัญหา ตามที่ยกตัวอย่างไปแล้ว
(ถ้าเป็นภาษาที่เป็น Purely FP คงไม่เกิดปัญหาเช่นนี้ง่ายๆ นะ)
ตัวอย่างที่ classic และเห็นบ่อยสุดๆ ก็คือ Number.parseInt
นั่นเอง
Number.parseInt
เป็น static method ที่รับ 2 argument อันได้แก่ string
และ radix
string
รับ string ใดๆ ก็ได้ (ถ้า string นั้นไม่ใช่ string ที่เป็นตัวเลข จะได้ output เป็นNaN
)radix
รับ number ตั้งแต่2
—36
เป็น param ที่ใช้บอกว่าเราจะแปลงให้เป็นเลขฐานอะไร โดยมี default value คือ10
หรือเลขฐาน 10
โดยที่เรามีเจ้า argument radix
กับ index
ของ mapper function ที่มี type เป็น number เหมือนกันนี่แหละ คือตัวปัญหาของ case นี้
const numerals: `${number}`[] = ['1', '2', '3', '4', '5']
const parsedNumbers = numerals.map(Number.parseInt)
// ^? number[]
console.log(parsedNumbers) // [1, NaN, NaN, NaN, NaN]
ตอนที่เราส่ง Number.parseInt
ไปเป็น argument
ก็คงมีความคาดหวังใช่ไหมล่ะว่า ผลลัพธ์ที่ได้จะต้องเป็น [1, 2, 3, 4, 5]
แน่ๆ เลย
แต่เปล่าฮะ นั่นเพราะพวกเราส่วนใหญ่คิดว่า parseInt มันรับแค่ argument เดียว
ซึ่งก็คือ string นั่นเอง
แต่ด้วยความที่มันมี radix
นั่นแหละ
เลยกลายเป็นว่า parseInt ได้รับทั้ง 2 arguments ในคราวเดียว
ผมจะเล่าเป็น code ให้ฟัง
numerals.map((x, index, _xs) => Number.parseInt(x, index))
นี่ล่ะ! ความลับของ bug ที่เกิดจาก Number.parseInt
+ point-free
แล้วแบบนี้เราจะ deal กับมันยังไงล่ะ?
ง่ายๆ เลย…เราก็ทำให้มันเป็น Unary Function เสียก่อนสิ
function parseIntWithoutRadix(x: number) {
return Number.parseInt(x)
}
const parsedNumbers = numerals.map(parseIntWithoutRadix)
console.log(parsedNumbers) // [1, 2, 3, 4, 5]
แค่นี้ เราก็มี unary function หรือฟังก์ชันที่รับ parameter ตัวเดียวที่พร้อมนำไปใช้เป็น mapper function แล้ว
ง่ายๆ แค่นี้จริงดิ?
ก็ใช่น่ะสิ ทำไมจะไม่ใช่ล่ะ?
มีวิธีอื่นอีกไหม?
มีนะ แต่คงต้องสร้างเป็น Utility Function แล้วนำไปใช้งาน
type AnyFunction = (...args[]) => any
export function unary<TFunction extends AnyFunction>(fn: TFunction) {
return function appliedUnary(arg: Parameters<TFunction>[0]): ReturnType<TFunction> {
return fn(arg)
}
}
จาก code snippet ข้างบน จะเห็นได้ว่า มีการ return ฟังก์ชัน appliedUnary
ออกมาจากฟังก์ชัน unary
ซึ่งเป็น utility function พระเอกของเรา
โดยที่ฟังก์ชัน appliedUnary
ได้มีการตัดตอน parameter ของ callback fn
ให้เหลือ parameter ตัวแรกเพียงตัวเดียว
แล้ว return การเรียกใช้ fn(arg)
ออกมา
นั่นจะทำให้ฟังก์ชันใดๆ ที่ส่งมาเป็น argument ของฟังก์ชัน unary
นี้กลายเป็น Unary Function ไปเองครับ
การนำ unary
utility function ไปใช้งาน
const parsedNumbers = numerals.map(unary(Number.parseInt))
console.log(parsedNumbers) // [1, 2, 3, 4, 5]
จบแล้วครับ ถ้ามีตรงไหนผิดพลาดก็ต้องขออภัยด้วยครับ
หรือถ้ามีจุดไหนที่อยากให้แก้ไข แล้ว comment บอกให้ผมทราบจะขอบคุณอย่างมากเลยครับโผม
Conclusion
โดยสรุปแล้ว point-free กับ unary function นี้มีประโยชน์มากๆ ในการเขียนโปรแกรมให้มันอธิบายตัวเองได้ง่ายๆ
แต่ทุกคนต้องเข้าใจและเชื่อใจใน utility function และ concept ที่พิสูจน์มาแล้วในทางคณิตศาสตร์ว่ามันเป็นจริงเช่นนั้นนะ
โดยความเชื่อใจในสิ่งเหล่านี้อาจมาจาก
- การมี utility function ที่มี Unit Test คลุม (แต่ของผมไม่มีอะ 555)
(หรืออาจจะไปใช้ FP Library อย่างเช่นlodash/fp
,ramda
,radash
ที่เขาเขียนกันเป็นมาตรฐานและมี Unit Test คลุมให้แล้วก็ได้นะ) - การมีความรู้ความเข้าใจบางส่วนใน Abstract math และ/หรือ Functional Programming
ถ้ามีทั้งสองอย่างนี้ได้ ผมว่า Dev หลายๆ คนที่ทำงานด้วยกันกันคงจะ happy กับการเขียนและอ่าน code ได้เข้าใจได้ง่ายมากๆ เลยล่ะครับ
แต่ถ้าเป็นอาจานเดฟ (ผู้สอนคลาส Maths for Working Programmer (ที่ผมอยากเห็นเพื่อนๆ น้องๆ ไปเรียนกับแกกันเยอะๆ))
ผมคิดว่าแกอาจจะช่วยเสริมว่า FP มัน Reasonable หรือให้เหตุผลได้ง่ายด้วยครับ
(แต่ผมยังไม่ค่อยเข้าใจในคำนี้เท่าไรว่า reason ได้ง่ายมันเป็นยังไง เลยไม่กล้าเคลมเอง 5555)
บทความที่เกี่ยวข้อง
บทความที่อาจไม่เกี่ยวข้อง (แต่อยากแชร์เพราะเห็นว่าน่าจะเป็นประโยชน์)
Useful Unix Commands to Kill Process that I usually use
Kill Process by Port number
fresult.medium.com
PS.
ผมไม่ได้เขียน blog เชิง technical มาสักระยะแล้ว
แต่ช่วงนี้ที่บริษัทกำลังผลักดันให้ team player ช่วยกันเขียน blog แบ่งปันความรู้ ไม่ว่าจะเรื่องเล็กน้อยแค่ไหนก็ตาม เพราะมันอาจจะสร้าง impact ให้กับทีม หรือสร้างประโยชน์ให้กับคนอื่นๆ ที่ได้เข้ามาอ่านก็ได้
เอาเป็นว่าผมขอเริ่มเป็นคนแรกแล้วกันฮะะ
นอกจากนั้นแล้วก็ยังมีประโยชน์จากการเขียน Blog อีกมากมาย คล้ายๆ กับการเขียน Document ที่ใน blog post และ talk สองอันนี้ได้กล่าวไว้
ผู้เขียน blog post เรื่องประโยชน์ของการเขียน Brag Document และผู้บรรยายในคลิปข้างบนนี้ คือคนเดียวกันครัช แต่สิ่งที่พูด/เขียนถึงจะมีความแตกต่างกันเยอะอยู่นะ
ตามไปอ่านไปฟังกันได้ตามอัฒยาศัยเลยค้าบ
อีกอันที่มีประโยชน์
References:
Thanks for reading until the final line.
If you’re confused or have any questions, don’t hesitate to leave the comments.
Feel free to connect with me and stay updated on the latest insights and discussions.
👉 https://linkedin.com/in/fResult 👈