uk
Feedback
ToCode

ToCode

Відкрити в Telegram

טיפים קצרים למתכנתים מאת ינון פרק

Показати більше
1 420
Підписники
Немає даних24 години
+27 днів
-230 день
Архів дописів
ToCode
1 420
# שלוש סיבות לבחור ב Tauri לבניית אפליקציית ה Desktop הבאה שלכם טאורי הוא כלי שמאפשר לכתוב אפליקציית Desktop בטכנולוגיות ווב. ואם המשפט הזה מזכיר לכם כלי אחר שמתחרז עם "עפרון", הנה שלוש סיבות שבגללן אני מתכנן לבחור בטאורי לפרויקט ה Desktop הבא שלי: 1. גודל הקובץ - יישום אלקטרון פשוט יתחיל ב 50 מגה ויכול להגיע בקלות לכמה מאות. טאורי לא כולל את כרומיום ומשתמש ב Web View שכבר קיים במערכת ההפעלה, לכן יישומים פשוטים לוקחים בסך הכל 3-4 מגה. 2. ראסט - טאורי משתמש ב Rust בשביל לתקשר עם מערכת ההפעלה ובנוי על תהליך Rust שמפעיל את ה Web View של מערכת ההפעלה ושם פותח את קבצי ה HTML/CSS/JS שלכם. 3. אבטחת מידע - לבעיות XSS באפליקציות אלקטרון היו השלכות הרסניות, כיוון שקוד ה JavaScript קיבל גישה מלאה למחשב (דרך APIs של אלקטרון). טאורי לוקח גישה יותר מאובטחת ונותן לקוד ה JavaScript גישה רק ל APIs שבחרנו בצורה מפורשת. רוצים לראות איך האפליקציה שלכם תעבוד בתור Desktop App? המדריך כאן מסביר איך להוסיף את טאורי לכל Web Application כדי להפוך אותו לאפליקציית Desktop, ולדעתי הוא מקום טוב להתחיל לשחק עם הכלי: https://tauri.app/v1/guides/getting-started/setup/integrate

ToCode
1 420
# שתי מוטיבציות יש מוטיבציה שנוצרת מפחד- לדוגמה הרבה אנשים אחרי ראיון עבודה יוצאים עם הרגשה של "אני לא יודע כלום", ואז כמו מקבלים זריקת מרץ ללמוד כמה שיותר כדי לבוא מוכנים לראיון הבא. הבעיה שזריקת המרץ הזאת לא מחזיקה מעמד הרבה זמן. מוטיבציה שנובעת מפחד נגמרת כשהפחד נגמר, ופחד הוא רגש זמני. (כשהפחד הופך לכרוני הוא כבר נקרא חרדה, וחרדה לא מניעה אף אחד לפעולה). הסוג השני של מוטיבציה נוצר מהשראה (Inspiration)- אנשים כמונו עושים דברים בצורה כזאת. הבחירה של מתכנתים רבים ללמוד טכנולוגיה חדשה כי "זה מעניין" או "זה כלי שחשוב להכיר" היא סוג כזה של מוטיבציה. אף אחד לא יפטר אותך אם לא תלמד Web Assembly, אבל הרבה מתכנתי ווב כבר רואים את הפוטנציאל ומתחילים ללמוד את הטכנולוגיה. שני הסוגים חשובים אבל רק את הסוג השני אנחנו צריכים לחפש באופן אקטיבי. כשאתם רוצים להתקדם, שווה להתאמץ ולמצוא השראה טוב יותר.

ToCode
1 420
import textReducer from './slices/text';
import saveForLaterReducer from './slices/save_for_later';
import { actions } from './slices/save_for_later';
import { actions as textActions } from './slices/text';

export const store = configureStore({
  reducer: {
    text: textReducer,
    saveForLater: saveForLaterReducer,
  },
})

setTimeout(() => {
  store.dispatch(actions.enqueue(textActions.change("New text from server")));
}, 5000);

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
בשביל הדוגמה במקום ליצור Web Socket בסך הכל הפעלתי Timer, אבל בתוכנית אמיתית ה setTimeout יוחלף ב Web Socket. החלק האחרון בפאזל הוא הקובץ App.tsx שמציג את הממשק. בממשק שלי יש בסך הכל תיבת טקסט שמשתמש יכול לשנות בה את הטקסט:
import './App.css'
import { useSelector, useDispatch } from 'react-redux'
import { RootState, AppDispatch } from './redux/store';
import { actions } from './redux/slices/text';
import { runPending } from './redux/slices/run_pending_thunk';
const useApplicationDispatch: () => AppDispatch = useDispatch;

function App() {
  const dispatch = useApplicationDispatch();
  const text = useSelector((state: RootState) => state.text.value);
  const hasPendingActions = useSelector((state: RootState) => state.saveForLater.pending.length > 0);

  const sync = () => {
    dispatch(runPending());
  }

  return (
    <div className="App">
      {hasPendingActions && (
      <p>
        New text from server available.
        <button onClick={sync}>Click To Sync</button>
      </p>)}
      <input type="text" value={text} onChange={(e) => dispatch(actions.change(e.target.value))} />
    </div>
  )
}

export default App
כשיש Actions בתור יופיע משפט שאומר שיש מידע חדש מהשרת וכפתור סינכרון. לחיצה על הכפתור תפעיל את ה thunk שיעשה dispatch לכל ה actions בתור וימחק אותם. נ.ב. הרבה פעמים לא נרצה לשמור את כל ה Actions מהשרת אלא רק את ה Action האחרון שהגיע מסוג מסוים, כי Action חדש יותר הופך את אלה שלפניו ללא רלוונטיים. לוגיקה כזאת תיכתב בתוך ה saveForLater Reducer בפונקציה enqueue:
enqueue(state, action: PayloadAction<Action>) {
  state.pending.push(action.payload);
}
פונקציה זו תוכל לבדוק אם ה Action שהגיע מייתר Action-ים ישנים יותר ולעדכן את המערך.

ToCode
1 420
# עידכון אפליקציית ריאקט אחרי קבלת אירוע מהשרת מערכות ווב מסורתיות היו צריכות להתמודד רק עם אירועים שמגיעים מהמשתמש שעכשיו גולש באתר. בעולם המודרני והמסוכנרן שלנו, מערכות ווב כבר צריכות להתמודד גם עם פעולות משתמש וגם עם אירועים שמגיעים מרחוק, הרבה פעמים מהשרת, ולסנכרן בין השניים. בואו נראה למה זה קשה ודרך אחת להתמודד עם האתגר באפליקציית ריאקט. ## למה זה קשה מעבר לרמה הטכנית של להשאיר Web Socket פתוח שמקבל הודעות מהשרת, אתגר יותר גדול הוא האתגר הממשקי: ברגע שיש שני מקורות של אירועים, משתמש יכול להתבלבל כי דברים משתנים על המסך בצורה אוטומטית. גוגל וורד למשל פותרים את הבעיה באמצעות הצגת סמני עכבר לכל המשתמשים שעכשיו עובדים על אותו המסמך. כשאני מעדכן את המסמך והוא משתנה לי מול העיניים, אני מבין שאני עובד עם עוד מישהו והשינוי מגיע מהבן אדם השני. בג'ימייל ההתנהגות שונה - אם אני כותב תגובה למייל ובאמצע הכתיבה מגיעה תגובה חדשה ממישהו אחר לאותו מייל, ג'ימייל יסמן לי שיש תגובה חדשה ויבקש ממני ללחוץ על כפתור כדי לטעון אותה. הם מבינים ששינוי המסך כשאני לא מצפה לשינוי עלול להיות מבלבל ולכן בחרו לממש את השינוי רק אחרי פעולה יזומה. ## איך עושים את זה בריאקט באפליקציית React ו Redux, מימוש מנגנון דומה הוא מאוד פשוט. זה מה שנעשה בגדול: 1. ניצור Web Socket שיקשיב להודעות מהשרת. 2. כל פעם שנקבל הודעה נקרא ל dispatch, אבל במקום לשלוח את ה Action שהיה משנה את המסך, נשלח Action אחר שעוטף אותו ושומר אותו בתור הודעות. 3. האפליקציה יכולה להסתכל אם יש Actions בתור, ואם כן להציג למשתמש הודעה שאומרת שיש מידע חדש מהשרת וצריך ללחוץ כדי לרענן. בלחיצה על הכפתור אנחנו פותחים את העטיפה ומפעילים dispatch על כל ה Actions ששמורים בתור. ## קוד לדוגמה אני יוצר אפליקציית React/Redux חדשה ובתוכה יוצר שני סלייסים. הסלייס הראשון יחזיק את המידע של האפליקציה, אני קורא לו text וזה הקוד שלו:
// file: src/redux/slices/text
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'

export interface TextState {
  value: string
}

const initialState: TextState = {
  value: "Hello World",
}

export const slice = createSlice({
  name: 'text',
  initialState,
  reducers: {
    change(state, action: PayloadAction<string>) {
      state.value = action.payload;
    }
  },
})

// Action creators are generated for each case reducer function
export const actions = slice.actions
export default slice.reducer
הסלייס השני אחראי על שמירת Actions שמגיעים מהשרת בצד כדי שאפשר יהיה "להריץ" אותן מאוחר יותר. אני אקרא לו save_for_later וזה הקוד בקובץ:
// file: src/redux/slices/save_for_later.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { Action } from '@reduxjs/toolkit'
import { clearPending } from './run_pending_thunk';

export interface SaveForLaterState {
  pending: Array<Action>;
}

const initialState: SaveForLaterState = {
  pending: [],
}

export const slice = createSlice({
  name: 'saveForLater',
  initialState,
  reducers: {
    enqueue(state, action: PayloadAction<Action>) {
      state.pending.push(action.payload);
    }
  },
  extraReducers: (build) => {
    build.addCase(clearPending, () => {
      return initialState;
    })
  }
})

// Action creators are generated for each case reducer function
export const actions = slice.actions
export default slice.reducer
בנוסף אני יוצר קובץ עם Thunk שיהיה אחראי על פתיחת כל ה Actions מתוך התור כשהמשתמש לוחץ על כפתור הסינכרון:
// file: src/redux/slices/run_pending_thunk.ts

import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
import { RootState } from "../store";

export const clearPending = createAction('saveForLater/clearPending');

export const runPending = createAsyncThunk<void, void, {state: RootState }>('saveForLater', async (arg: void, { getState, dispatch }) => {
  const pending = getState().saveForLater.pending;
  pending.forEach(dispatch);
  dispatch(clearPending());
});
הקובץ store.ts נראה כך:
// file: src/redux/store.ts

import { configureStore } from '@reduxjs/toolkit'

ToCode
1 420
# חדש ב Ruby: אוביקט מידע רובי 3.2 יצאה לא מזמן וכולם מדברים על התמיכה ב Web Assembly שהיא הוסיפה - וזה באמת פיצ'ר מדליק שפותח הרבה אפשרויות, אבל לא בטוח איך הוא הולך לעזור לי בכתיבת קוד ביום יום. פיצ'ר אחר שזכה לקצת פחות כותרות נראה לי הרבה יותר שימושי ועליו הפוסט היום והוא נקרא פשוט Data. כן דומה מאוד ל Data Class של פייתון. איך זה עובד? אז Data מספק דרך להגדיר אוביקט מידע שלא ניתן לשינוי, אפשר להשוות בין אוביקטי מידע כאלה והוא שומר על הייצוג שלו בכל מקום. זה קצת דומה ל Struct אבל עם פחות אפשרויות וממשק קל יותר. לדוגמה אם אני רוצה לייצג במערכת משתמש שיש לו מזהה מספרי וכתובת אימייל כדי להעביר בין כמה מקומות במערכת אני יכול ברובי 3.2 לכתוב משהו כזה:
User = Data.define(:name, :email)

u1 = User.new(name: 'dave', email: 'dave@e.com')
u2 = User.new(name: 'john', email: 'john@e.com')

puts u1.email

puts "u1 == u2 ? #{u1 == u2}"
עד לפה הכל קל ודומה ל Struct, אבל יש שני הבדלים מרכזיים: 1. אוביקטי Data לא מגדירים את to_a לכן אי אפשר להמיר Data Object למערך של כל הערכים שבו (כמובן שאין בעיה להפעיל to_h כדי לקבל Hash) 2. אוביקטי Data לא ניתנים לשינוי, וכך שומרים עלינו שלא נעשה טעויות מוזרות - למשל נקבל אוביקט Data באיזו פונקציה, נשנה אותו וכך נגרום לנזק במקום אחר בקוד. סך הכל שינוי קטן שיכול להפוך קוד רובי לקצת יותר נעים.

ToCode
1 420
# נכשלתי במבחן פישינג (או: למה לינקים במייל זה כל כך מסובך) מייקרוסופט החליטו לעשות לי מבחן פישינג ושלחו לי מייל שמספר לי שחבר שיתף איתי הערות על פרויקט. הכרתי את שם החבר אבל היה נראה לי מוזר שהוא משתף הערות על פרויקט כי אנחנו לא עובדים על אף פרויקט יחד עכשיו, לכן לחצתי על הקישור כדי לראות על מה מדובר והגעתי לדף שמסביר שזה היה רק תרגיל פישינג ובעצם אף אחד לא שיתף איתי כלום. לקח כמה רגעים להתאושש מהאכזבה, אחריהם ניסיתי לשחזר איך זה קרה ולמה לינקים במייל זה כל כך מסובך. נתחיל באמת הפשוטה - לינקים במייל באמת יכולים להיות מסוכנים. אלה הדברים הרעים המרכזיים שיכולים לקרות כשאנחנו לוחצים על לינק בלי לדעת לאן הוא מוביל: 1. יש אנשים שפשוט רוצים לדעת שמישהו קורא את המיילים שלהם, כדי שיוכלו לשלוח עוד ועוד מיילים. לחיצה על לינק מאשרת לאותם אנשים שכתובת המייל שלכם אמיתית ואתם אנשים אמיתיים שקוראים דואר בכתובת הזאת. 2. יש אתרים עם באגים, במיוחד באגים כמו XSS ו CSRF. באג דמיוני כזה יכול לגרום לכם לעשות בלי כוונה פעולה באתר שאתם כבר מחוברים אליו מטאב אחר. לדוגמה נדמיין שאני מחובר לפייפאל שלי מטאב אחר, ובפייפאל יש כפתור ששולח כסף לחבר, אז בגלל באג באתר תוקף יכול לשלוח אליי לינק לדף מיוחד בפייפאל שכשאני אכנס אליו אתר פייפאל יחשוב שלחצתי על כפתור לשליחת כסף לאותו תוקף. 3. יש באגים בדפדפנים. ברור שאם חברת גוגל או מייקרוסופט ישמעו על באג בדפדפן שלהן הן יפיצו מיד תיקון, אבל הרבה פעמים אתם לא משדרגים גירסה בדפדפן מיד כשהוא מבקש לשדרג, ועוד יותר הרבה פעמים אנשים שומרים בסוד את הבאגים ולא מספרים עליהם ליצרניות הדפדפנים. באג בדפדפן יכול לגרום לדפדפן שלכם להריץ תוכנית זדונית רק בגלל שלחצתם על לינק. בהתחשב בכל הסכנות האלה אפשר היה לדמיין שנהיה יותר זהירים בלחיצות על לינקים, אבל יש בעיה - לחיצה על לינקים עדיין מהווה חלק מרכזי בעבודה היום יומית שלנו ובאינטרקציה עם המחשב. כמה דוגמאות מתיבת המייל שלי: 1. הזמנה לשיחות עבודה בזום או בטימס מגיעות למייל, ונכנסים אליהן באמצעות לחיצה על הלינק. 2. עדכונים על PR בגיטהאב מגיעים למייל עם לינק ל PR. 3. ניוזלטרים שנשלחים למייל מגיעים מלאים בלינקים, ואנחנו רגילים ללחוץ על לינקים למאמרים שמעניינים אותנו. 4. אחרי קניה באמזון תקבלו מייל עם פרטי הזמנה ואפשרות ללחוץ על לינק כדי להיכנס לדף ההזמנה שהרגע ביצעתם. 5. חברות רבות ישלחו חשבונית בתור קובץ מצורף, אבל חברות רבות אחרות ישלחו אותה בתור לינק לקובץ PDF ברשת. נסו יום אחד לרשום את כל הלינקים שאתם לוחצים עליהם מהמייל ותגלו שזה לא משהו שאפשר לעצור. בנוסף אנחנו יודעים שכתובת מייל שולח היא לא דרך אמינה לאמת את מקור המייל, ומיילים זדוניים יכולים להגיע גם מתיבות דואר של אנשים שאנחנו סומכים עליהם. בנוסף אפילו אם במייל כתוב בפירוש הלינק, הלחיצה עליו יכולה להוביל למקום אחר מזה שכתוב (כי הטקסט של הלינק יכול להיות שונה מכתובת היעד) ולא נראה את זה עד שנלחץ. אפילו אם ננסה להעתיק את הלינק הצידה ולסרוק אותו בעין לפני שניכנס לאתר זה לא מספיק טוב, כי לינק לאתר זדוני יכול להיראות בדיוק כמו לינק לאתר רגיל, או שהלינק יכיל שגיאת כתיב קטנה שבקריאה רגילה לא נשים לב אליה. וכמובן לינקים באימייל הרבה פעמים שולחים אתכם דרך "לינק מתווך" של חברת הדיוור כדי שיוכלו לדעת מי לחץ על איזה לינק. פיתרון? אני לא רואה באופק. ברמה הפרקטית העצות הנפוצות עדיין נכונות - לא ללחוץ על לינקים מזרים, לא לפתוח מייל מאנשים שלא מכירים וכו', אבל ברמת האבטחה זה רחוק מ 100%. יש גם לא מעט תוכנות שתפקידן לזהות פישינג או לינקים חשודים, אבל קשה לראות את רובנו עוברים להתקין ולשלם על פיתרונות כאלה. פיתרון ברמת ספק המייל יכול לעזור אבל גם זה לא מושלם כי לינקים מגיעים גם דרך הווטסאפ או טלגרם.

ToCode
1 420
[?dir :entry/name ?name]
       [?dir :entry/type :entry-dir]
       (inside ?child ?dir)
       [?child :entry/size ?size]] (d/db conn) rules)

ToCode
1 420
2. יש יחס במערכת שנקרא inside. כשמשהו inside משהו אחר זה אומר שהוא belongs-to הדבר האחר, או שה parent שלו הוא inside הדבר האחר. וכן ההגדרה רקורסיבית והכל בסדר. עכשיו השאילתה משתמשת בהגדרה כדי למצוא את כל התיקיות והגדלים שלהן:
(d/q '[:find ?name (sum ?size)
       :in $ %
       :where
       [?dir :entry/name ?name]
       [?dir :entry/type :entry-dir]
       (inside ?child ?dir)
       [?child :entry/size ?size]] (d/db conn) rules)
       
;; returns [["" 48381165] ["a" 94853] ["d" 24933642] ["e" 584]]
       
ומכאן אפשר לקחת את הרשימה ולהשתמש בקוד כדי לענות על השאלות. קוד התוכנית המלא בקלוז'ר הוא:
(ns main
  (:require [datomic.api :as d]))

(def db-uri "datomic:mem://fs99")

(d/create-database db-uri)

(def conn (d/connect db-uri))

(def schema
  [
   {:db/ident :entry/type
    :db/valueType :db.type/ref
    :db/cardinality :db.cardinality/one}
   {:db/ident :entry/name
    :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one}
   {:db/ident :entry/link-to
    :db/valueType :db.type/ref
    :db/cardinality :db.cardinality/one}
   {:db/ident :entry-link}
   {:db/ident :entry-file}
   {:db/ident :entry-dir}
   {:db/ident :entry/parent
    :db/valueType :db.type/ref
    :db/cardinality :db.cardinality/one}
   {:db/ident :entry/size
    :db/valueType :db.type/long
    :db/cardinality :db.cardinality/one}
   {:db/ident :fs/id
    :db/cardinality :db.cardinality/one
    :db/unique :db.unique/identity
    :db/valueType :db.type/string}
   {:db/ident :fs/cwd
    :db/cardinality :db.cardinality/one
    :db/valueType :db.type/ref}
   ])

@(d/transact conn schema)

(defn cwd []
  (let [db (d/db conn)]
    (d/q '[:find ?cwd .
           :where
           [?fs :fs/id "/"]
           [?fs :fs/cwd ?cwd]] db)))

(defn file [name size]
  @(d/transact conn [
                     {:entry/type :entry-file
                      :entry/name name
                      :entry/size size
                      :entry/parent (cwd)}
                     ]))

(defn dir [name]
  @(d/transact conn [
                     {:entry/type :entry-link
                      :entry/parent "d"
                      :entry/name ".."
                      :entry/link-to (cwd)}
                     {:entry/type :entry-link
                      :entry/parent "d"
                      :entry/name "."
                      :entry/link-to "d"}
                     {:db/id "d"
                      :entry/type :entry-dir
                      :entry/name name
                      :entry/parent (cwd)}
                     ]))


(defn cd [name]
  (let [db (d/db conn)
        to (d/q '[:find ?e .
                  :in $ ?cwd ?name
                  :where
                  [?e :entry/name ?name]
                  [?e :entry/parent ?cwd]]
                db
                (cwd) name)
        toe (d/entity db to)
        tod (if (= (:entry/type toe) :entry-link)
              (:db/id (first (filter #(= (:entry/type %1) :entry-dir) (iterate :entry/link-to toe))))
              to)]
    @(d/transact conn [{:fs/id "/"
                        :fs/cwd tod}])))

(def data [
           {:db/id "root" :entry/name "" :entry/type :entry-dir}
           {:fs/id "/" :fs/cwd "root"}
           ])

@(d/transact conn data)

(dir "a")
(file "b.txt" 14848514)
(file "c.dat" 8504156)
(dir "d")
(cd "a")
(dir "e")
(file "f" 29116)
(file "g" 2557)
(file "h.lst" 62596)
(cd "e")
(file "i" 584)
(cd "..")
(cd "..")
(cd "d")
(file "j" 4060174)
(file "d.log" 8033020)
(file "d.ext" 5626152)
(file "k" 7214296)

(d/q '[:find ?name :where
       [?e :entry/type :entry-file]
       [?e :entry/name ?name]] (d/db conn))

(def rules
'[[(belongs-to ?entry ?parent)
   [?entry :entry/parent ?parent]]
  [(inside ?entry ?parent)
   (belongs-to ?entry ?parent)]
  [(inside ?entry ?parent)
   (belongs-to ?entry ?middle)
   (inside ?middle ?parent)]])

(d/q '[:find ?name (sum ?size)
       :in $ %
       :where

ToCode
1 420
[?fs :fs/cwd ?cwd]] db)))
והנה אינטרקציה ראשונה עם Datomic. התיקיה הנוכחית נשמרת בבסיס הנתונים בתור הערך של מאפיין :fs/cwd. בסיס הנתונים תומך במספר מערכות קבצים במקביל אבל בתוכנית שלי השתמשתי רק במערכת קבצים אחת ונתתי לה את המזהה /. בהמשך נצטרך לראות איך להכניס את מערכת הקבצים הזו לבסיס הנתונים. הפונקציה הבאה היא file והיא יוצרת קובץ בתיקיה הנוכחית:
(defn file [name size]
  @(d/transact conn [
                     {:entry/type :entry-file
                      :entry/name name
                      :entry/size size
                      :entry/parent (cwd)}
                     ]))
הפקודה transact של Datomic מגדירה טרנזאקציה וזו בעצם עובדה חדשה שאני מכניס לבסיס הנתונים - העובדה שיש דבר בשם name, שה parent שלו הוא התיקיה הנוכחית, שהגודל שלו הוא size ושהסוג שלו הוא :entry-file. הפונקציה הבאה היא dir והיא קצת יותר מסובכת מ file, בגלל שבכל תיקיה יש לנו שני לינקים מיוחדים - הקישור . שמחבר אותה לעצמה והקישור .. שמחבר אותה לתיקיה שמעליה:
(defn dir [name]
  @(d/transact conn [
                     {:entry/type :entry-link
                      :entry/parent "d"
                      :entry/name ".."
                      :entry/link-to (cwd)}
                     {:entry/type :entry-link
                      :entry/parent "d"
                      :entry/name "."
                      :entry/link-to "d"}
                     {:db/id "d"
                      :entry/type :entry-dir
                      :entry/name name
                      :entry/parent (cwd)}
                     ]))
לכן הטרנזאקציה הפעם מספרת שלוש עובדות חדשות: 1. יש תיקיה בשם name וההורה שלה הוא parent 2. יש לינק בשם . וההורה שלו הוא התיקיה החדשה שתיווצר. הלינק מוביל לאותה תיקיה חדשה. 3. יש לינק בשם .. וההורה שלו הוא התיקיה החדשה שתיווצר. הלינק מוביל ל parent. הגדרת המזהה d בתוך הטרנזאקציה היא שמאפשרת בקלות לכמה עובדות להתיחס לאותה ישות. והפונקציה המסובכת ביותר בקוד היא פונקציית cd. מקור הסיבוך הוא התמיכה בלינקים, כי אם אני עושה cd ללינק אני מצפה להגיע לדבר שהלינק מצביע אליו. הקוד בדוגמה תומך גם בלינק שמפנה ללינק, אבל ייכשל בצורה אומללה עם מעגלים:
(defn cd [name]
  (let [db (d/db conn)
        to (d/q '[:find ?e .
                  :in $ ?cwd ?name
                  :where
                  [?e :entry/name ?name]
                  [?e :entry/parent ?cwd]]
                db
                (cwd) name)
        toe (d/entity db to)
        tod (if (= (:entry/type toe) :entry-link)
              (:db/id (first (filter #(= (:entry/type %1) :entry-dir) (iterate :entry/link-to toe))))
              to)]
    @(d/transact conn [{:fs/id "/"
                        :fs/cwd tod}])))
ב Datomic, בשביל לקחת משהו שחוזר מ find ולחפש מאפיינים נוספים שלו אני צריך להשתמש בפונקציה שנקראת entity. הפונקציה מחזירה אוביקט מידע מלא מבסיס הנתונים שכבר כולל גם את כל האוביקטים שמקושרים אליו, וכך אפשר "לטייל" בעץ עד שמאפיין link-to מביא אותי לתיקיה. אחרי שהגדרנו את פוקנציות העזר אפשר לאתחל את מערכת הקבצים עם:
(def data [
           {:db/id "root" :entry/name "" :entry/type :entry-dir}
           {:fs/id "/" :fs/cwd "root"}
           ])

@(d/transact conn data)
ולהפעיל את התוכנית. ## הצגת כל התיקיות והגדלים בצורה רקורסיבית כל העבודה הקשה עד לפה מביאה אותנו לשאילתה האחרונה והמעניינת מכולן. אני רוצה למצוא את כל התיקיות ולכל תיקיה את הגודל שלה, שמורכב מהגודל של כל הילדים שלה. בשביל זה אני צריך להגדיר ל Datomic מה זה נקרא שמשהו נמצא רקורסיבית בתוך תיקיה:
(def rules
'[[(belongs-to ?entry ?parent)
   [?entry :entry/parent ?parent]]
  [(inside ?entry ?parent)
   (belongs-to ?entry ?parent)]
  [(inside ?entry ?parent)
   (belongs-to ?entry ?middle)
   (inside ?middle ?parent)]])
הביטוי rules מייצג סט של ״חוקים״ שאיתם אפשר לגזור מידע חדש מהעובדות שלנו. משמעות החוקים שהגדרתי היא: 1. יש יחס במערכת שנקרא belongs-to. כשמשהו belongs-to משהו אחר זה אומר שמשהו אחר הוא ה parent של משהו.

ToCode
1 420
# פיתרון תרגיל Advent Of Code יום 7 בעזרת Datomic לפני כמה ימים סיפרתי כאן על Datomic, בסיס נתונים שמאחסן "עובדות" ומציע שפת שאילתות בשם Datalog כדי לחפש מידע ומסקנות שקשור לעובדות אלה. אחד השימושים המדליקים ב Datomic הוא היכולת לחפש בצורה רקורסיבית במבני נתונים מקושרים כמו גרפים או עצים. בזכות Datalog, שאילתות שהיו יכולות להיות מסובכות מאוד ב SQL הופכות לכמה שורות. בפוסט זה נראה דוגמה לאחסון מידע על מערכת קבצים ב Datomic, ואיך אחסון כזה עוזר לנו לחשב מהר גדלים בצורה רקורסיבית. ## האתגר לקחתי את התרגיל שפורסם ב Advent Of Code Day 7 בשביל דוגמה למשימה על גרפים. התרגיל בגדול נותן לנו מידע על קבצים, תיקיות וגדלים ואחרי זה שולח אותנו לחפש תיקיות בגדלים מסוימים. המידע מגיע בפורמט הבא:
$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
$ ls
4060174 j
8033020 d.log
5626152 d.ext
7214296 k
שנראה כמו לקוח משורת פקודה ביוניקס. יש לנו כאן שורות מכמה סוגים: 1. שורות שמתחילות בדולר מסמנות שפה יש "פקודה" ואחריה יבואו השורות שמתאימות לפלט שלה. אחרי פקודת ls נקבל את רשימת הקבצים והתיקיות בתיקיה הנוכחית, אחרי פקודת cd אין פלט אבל התיקיה תשתנה. 2. שורות שמתחילות במספר מסמנות שיש קובץ בגודל מסוים בתיקיה הנוכחית. המספר הוא הגודל והטקסט הוא שם הקובץ. 3. שורות שמתחילות במילה dir אומרות שיש תיקיה בשם מסוים בתיקיה הנוכחית. אחרי שנפענח את הקלט נצטרך לענות על שתי שאלות שקשורות לגדלים: 1. נרצה למצוא את סכום הגדלים בכל התיקיות שגודלן קטן או שווה ל 100000 2. נרצה למצוא את התיקיה הקטנה ביותר שגודלה גדול מ 8381165 בשביל להתמקד ב Datomic ולא בפיענוח קלט או לולאות, אני אעשה כמה הנחות בתרגיל: את הקלט אני אכתוב ישירות לתוך התוכנית ואני אסתפק בלמצוא את כל התיקיות ואת הגודל (הרקורסיבי) של כל תיקיה, כלומר הגודל של התיקיה ושל כל הקבצים והתיקיות שבתוכה. ## סכימה לייצוג המידע בסיס נתונים Datomic משתמש בסכימה כדי להגיד איזה עובדות אפשר להכניס למערכת. הסכימה שלי נראית כך:
(def schema
  [
   {:db/ident :entry/type
    :db/valueType :db.type/ref
    :db/cardinality :db.cardinality/one}
   {:db/ident :entry/name
    :db/valueType :db.type/string
    :db/cardinality :db.cardinality/one}
   {:db/ident :entry/link-to
    :db/valueType :db.type/ref
    :db/cardinality :db.cardinality/one}
   {:db/ident :entry-link}
   {:db/ident :entry-file}
   {:db/ident :entry-dir}
   {:db/ident :entry/parent
    :db/valueType :db.type/ref
    :db/cardinality :db.cardinality/one}
   {:db/ident :entry/size
    :db/valueType :db.type/long
    :db/cardinality :db.cardinality/one}
   {:db/ident :fs/id
    :db/cardinality :db.cardinality/one
    :db/unique :db.unique/identity
    :db/valueType :db.type/string}
   {:db/ident :fs/cwd
    :db/cardinality :db.cardinality/one
    :db/valueType :db.type/ref}
   ])
במערכת יש שני סוגים של "דברים", לדבר אחד אפשר לקרוא "מערכת קבצים", וזה משהו ששומר את התיקיה הנוכחית. לדבר השני קראתי entry וזה יכול להיות קובץ, תיקיה או לינק בתוך מערכת הקבצים. לכל entry יש שם, יש את הסוג שלו ויש parent שזה ה entry שמכיל אותו. ## הכנסת מידע לבסיס הנתונים אחרי שיש לנו סכימה אפשר לנסות להכניס את המידע. כבר אמרתי שאני עושה פה הנחה ולא אכתוב את הקוד שיפענח את הקלט האמיתי, אבל כן רציתי שתוכנית הקלוז'ר שאכתוב כדי להכניס את המידע תהיה כמה שיותר דומה לקלט האמיתי, ברמה של משהו שאפשר ליצור אוטומטית עם sed. אחרי מחשבה הלכתי על המבנה הבא:
(dir "a")
(file "b.txt" 14848514)
(file "c.dat" 8504156)
(dir "d")
(cd "a")
(dir "e")
(file "f" 29116)
(file "g" 2557)
(file "h.lst" 62596)
(cd "e")
(file "i" 584)
(cd "..")
(cd "..")
(cd "d")
(file "j" 4060174)
(file "d.log" 8033020)
(file "d.ext" 5626152)
(file "k" 7214296)
בשביל שזה יעבוד צריך להגדיר את הפונקציות dir, file ו cd. בשבילן גיליתי שמאוד עוזר להגדיר פונקציה בשם cwd שמחזירה את התיקיה הנוכחית. נתחיל עם הקוד שלה כי כל הפונקציות האחרות ישתמשו בה:
(defn cwd []
  (let [db (d/db conn)]
    (d/q '[:find ?cwd .
           :where
           [?fs :fs/id "/"]

ToCode
1 420
# כשיש לך פטיש בהינתן בעיה, כלי העבודה שאנחנו בוחרים יהיה כמעט תמיד מתוך סט הכלים שאנחנו מכירים. לכן המשפט המפורסם יהיה נכון יותר אם ננסח "כשראית רק פטישים בעבר, כל דבר נראה כמו מסמר". וכאן יש גם שתי הזדמנויות ללמידה- 1. כדאי לחפש אנשים שראו דברים אחרים ממך. ככל שנמצא כאלה כך נכיר שיטות חדשות לפתור בעיות ודרכי מחשבה שאולי לא חשבנו עליהן. 2. כדאי ללמוד כלים חדשים, גם אם הם לא יתנו את הפיתרון הטוב ביותר לבעיה, ואפילו שווה לפתור את אותה בעיה כמה פעמים עם כלים אחרים. כי אם סט הכלים שאנחנו מכירים קובע את סט הפיתרונות שנוכל להציע, יותר כלים מובילים ישירות לפיתרונות יותר מעניינים.

ToCode
1 420
ומעניין לשים לב שגם הסכימה היא פשוט אוסף של עובדות, ואין הבדל בין לשדר את העובדות לגבי הפוסטים או המנויים לבין שידור העובדות של הסכימה. עכשיו בואו נראה קצת שאילתות. השאילתה הראשונה מחזירה את הכותרות של כל הפוסטים:
(d/q '[:find ?title :where [_ :post/title ?title]] (d/db conn))

;; returns: #{["second post"] ["rust"] ["first post"]}
אפשר גם לקבל את כל הפוסטים יחד עם ה slug של כל פוסט:
(d/q '[:find ?title ?slug :where
       [?p :post/title ?title]
       [?p :post/slug ?slug]] (d/db conn))
       
;; returns #{["rust" "rust"] ["second post" "second"] ["first post" "first"]}
ופה כדאי לשים לב לשימוש במשתנה ?p - בעצם ביקשתי מדטומיק להחזיר מידע על ישויות לפי שתי עובדות על הישויות, גם שלישות יש :post/title וגם שיש לה :post/slug. בגלל ששני התנאים מתיחסים לאותו משתנה ?p קיבלתי רק ישויות שמחזיקות עובדות משני הסוגים. אם הייתי משתמש שם ב _ במקום, הייתי מקבל משהו שדומה ל Outer Join ב SQL, כלומר את כל ה title-ים של כל הישויות ואחרי זה את כל ה slug-ים של כל הישויות, כלומר את הרשימה:
(d/q '[:find ?title ?slug :where
       [_ :post/title ?title]
       [_ :post/slug ?slug]] (d/db conn))

;; returns
;; #{["rust" "first"] ["rust" "rust"] ["first post" "second"] ["second post" "rust"] ["first post" "rust"] ["second post" "first"] ["second post" "second"] ["rust" "second"] ["first post" "first"]}
שאילתה יותר מעניינת תחזיר את כל הפוסטים שמעניינים משתמש מסוים:
(d/q '[:find ?title :where
       [?p :post/title ?title]
       [?p :post/categories ?c]
       [?s :subscriber/email "all@demomail.com"]
       [?s :subscriber/categories ?c]] (d/db conn))

;; returns #{["second post"] ["rust"] ["first post"]}
זה עובד כי המשתנה ?c מופיע גם ברשימת הקטגוריות של הפוסט וגם ברשימת הקטגוריות של המנוי. דטומיק מבין שזה אותו משתנה ולכן מחפש פוסטים ששלישיית הקטגוריות שלהן כוללת איזושהי קטגוריה מבין הקטגוריות של המנוי. בשביל לסמן שמנוי מסוים קיבל פוסט מסוים אני משתמש בטרנזאקציה הבאה:
@(d/transact conn [
                   {:subscriber/email "clojure@demomail.com"
                    :subscriber/received {:db/id [:post/slug "first"]}}])
ובגלל ה Cardinality של המאפיין received דטומיק יודע להוסיף את הפוסט שביקשתי לרשימה שם במקום להחליף ערך בודד. עכשיו אפשר להמשיך ולשאול איזה פוסטים משתמש מסוים כבר קיבל:
(d/q '[:find ?title :where
       [?p :post/title ?title]
       [?s :subscriber/email "clojure@demomail.com"]
       [?s :subscriber/received ?p]] (d/db conn))

;; #{["first post"]}
ויותר מעניין, איזה פוסטים שהמשתמש רשום לקבל הוא עדיין לא קיבל:
(d/q '[:find ?post-slug
       :where
       [?subscriber :subscriber/email "clojure@demomail.com"]
       [?subscriber :subscriber/categories ?cat]
       [?post :post/categories ?cat]
       (not-join [?subscriber ?post]
                 [?subscriber :subscriber/received ?post])
       [?post :post/slug ?post-slug]] (d/db conn))
       
;; returns #{["second"]}
שפת השאילתות datalog מתגלה כשפה מאוד ידידותית וקריאה, במיוחד בהשוואה ל SQL. וכן זו שפה חדשה וצריך ללמוד אותה, אבל אני מקווה שכבר מהדוגמאות הקטנות כאן אפשר לראות את הכח שלה. ## מה הלאה המקום הכי טוב ללמוד בו עוד על Datomic הוא התיעוד שלהם, שקצת קצר בדוגמאות אבל כולל אינסוף הסברים מקיפים: https://docs.datomic.com/on-prem/getting-started/brief-overview.html. אם אתם בעניין הרצאות ביוטיוב אז כאן ריק היקי מסביר על דטומיק: https://www.youtube.com/watch?v=9TYfcyvSpEQ וכאן יש הרצאה יותר טכנית על איך עובדים איתו ואיך מנהלים מידע: https://www.youtube.com/watch?v=yWdfhQ4_Yfw ואחרונה על שפת השאילתות datalog: https://www.youtube.com/watch?v=bAilFQdaiHk