무엇인가

PyMini는 Swift로 쓴 파이썬 부분집합 인터프리터이며 키보드 안에 들어 있습니다. 직접 만든 패널과 액션은 모두 그 위에서 돌아갑니다. 이미 알고 있는 모양의 파이썬을 읽고, 화면을 그리는 스크립트든 낱말을 고쳐 쓰는 스크립트든 같은 언어로 씁니다.

# an action: text in, text out
def transform(text):
    return text.upper()

# a panel: a view, and what a tap does
def view(state):
    return button("Shout", "shout")

def on_action(action, state):
    insert(context()["word"].upper())
    return state
언어는 하나, 계약은 둘. 액션은 transform(text), 패널은 view(state)입니다.

사용하는 이유

PyMini를 만든 이유는 Clink에 누구나 자기 키보드를 스크립트로 꾸밀 방법이 필요했는데, 키보드 안에 들어갈 만한 것을 찾지 못했기 때문입니다. 그런데 키보드에 묶인 부분은 전혀 없어서, 이제는 독립된 Swift 패키지로 오픈 소스가 되었습니다. 사용자가 Python을 조금 쓸 수 있게 하고 싶은 앱이라면 어디든 쓸 수 있습니다.

CPython이 아니고, 품고 있지도 않습니다. 키보드 확장이 받는 메모리는 앱의 일부에 불과해서, CPython의 바이너리와 힙만으로도 스크립트가 돌기 전에 그 예산을 다 써 버립니다. PyMini는 바이너리 의존성이 없는 수백 킬로바이트의 Swift입니다.

사용자가 자기만의 로직을 원하는 곳이라면 어디든 어울립니다. 글쓰기 앱의 텍스트 변환, 할 일이나 가계부 앱의 규칙, 수식, 자동화, 게임 모드, 커뮤니티가 있는 앱의 플러그인 시스템 같은 곳이죠. Python을 조금이라도 아는 사람이 많으니 새 언어를 가르칠 필요가 없고, 스크립트는 평범한 텍스트라서 다른 설정처럼 저장하고 동기화하고 공유할 수 있습니다.

import PyMini

let py = PyRuntime(modules: ["math", "json", "re"])

// your app, as a Python function
py.define("shout") { call in
    try call.argument(0, as: String.self).uppercased()
}

// a script one of your users wrote
try py.load("""
def tidy(text):
    return shout(" ".join(text.split()))
""")

try py.call("tidy", "  hello    there ")  // HELLO THERE
스크립트에 Swift 함수를 넘기고, 스크립트를 불러와 호출합니다.
호출하는 일
import PyMini의존성이 없고 따로 챙겨 넣을 것도 없습니다. Swift 패키지를 추가하면 iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1 이상에서 동작합니다.
py.define("shout") { call in ... }앱의 기능이 Python 함수와 모듈이 됩니다. 인수는 Swift 타입으로 들어오고, 돌려준 값은 Python 값으로 스크립트에 돌아갑니다.
PyLimits(steps: 200_000)모든 호출은 스텝 예산 안에서 돌고, 예산이 바닥나면 어떤 스크립트도 잡을 수 없는 오류가 됩니다. 무심코 쓴 while True도 앱을 멈추게 하는 대신 메시지로 끝납니다.
PyRuntime(modules: ["math"])스크립트는 파일도, 네트워크도, 프로세스도 쓸 수 없습니다. 닿을 수 있는 건 직접 정의한 함수와 켜 둔 표준 모듈뿐입니다.
py.call(hook: "on_open")앱에서 무슨 일이 생기면 스크립트를 호출하세요. 스크립트가 정의하지 않은 훅은 건너뛰니, 모든 훅을 선택 사항으로 둘 수 있습니다.
catch let error as PyError오류는 Python 예외 이름과 줄 번호와 함께 돌아오므로, 스크립트를 쓴 사람에게 바로 보여 줄 수 있습니다.
import syssys.version은 인터프리터 이름과 버전을 담고 있고, pymini 명령도 같은 줄을 출력합니다. 덕분에 스크립트가 자기를 실행하는 앱이 어떤 빌드를 품고 있는지 알려 줄 수 있습니다.

소스 코드, 언어 레퍼런스, 테스트는 GitHub에 있습니다.

무엇을 약속하는가

스크립트가 닿을 수 있는 파일 시스템도, 네트워크도, 프로세스도 없습니다. 샌드박스는 거부 목록이 아니라 인터프리터의 생김새 자체입니다. 바깥으로 난 문은 import 하나뿐이고, 그 문은 계산만 하는 몇 개의 네임스페이스로 이어집니다. 나머지는 모두 단계를 씁니다. 식 하나, 반복 한 바퀴, 정규식 한 단계, JSON 한 줄이 각각 2,000,000 중 하나를 쓰고, 문자열과 리스트는 자라는 대로 상한이 걸립니다.

공유 패널에서 가져올 수 있는 것

  • json
  • math
  • random
  • re
  • sys
  • time

이 예산은 잡을 수 없습니다. except 절은 KeyError를 잡지만, 폭주하는 반복을 끝내는 중단은 잡지 못합니다. 그것을 잡는 것이야말로 폭주한 스크립트가 계속 도는 방법이기 때문입니다.

변경 내역

PyMini의 모든 버전을 최신순으로, 각 버전에서 추가된 내용과 함께 정리했습니다.

PyMini 2.0

첫 번째 버전이 할 수 있던 일은 낱말을 대문자로 바꾸고 버튼을 격자로 늘어놓는 것이었습니다. 두 번째 버전은 작은 프로그램을 쓸 수 있는 언어이고, 정해진 반응을 지니고만 있는 대신 탭에 답하는 패널입니다.

예시바뀐 점
[word for word in words if word]컴프리헨션, lambda, 튜플, 집합, 그리고 try / except / finally / raise.
f"{total:,.2f}"f-문자열 변환과 완전한 서식 지정. str.format, format()과 같은 것을 씁니다.
f"""Hello {name}"""세 겹 따옴표 문자열, raw 문자열, 여러 줄에 걸친 f-문자열.
import math, random, time, json, re네임스페이스는 다섯 개뿐이고, 모두 순수합니다.
re.sub(r"\s+", " ", text)정규식은 Clink가 만든 엔진에서 돌아가며, 같은 단계 예산을 씁니다.
on_action(action, value, state)패널이 스스로 탭을 처리하고, 진짜 상태를 지니며, 컨트롤이 열한 개 늘었습니다.
replace(len(word), word.upper())스크립트가 키보드에 동작을 요청할 수 있고, 커서 주변도 읽을 수 있습니다.
Current sentence액션은 입력란의 어느 부분이든 읽습니다. 낱말과 그 앞부분만이 아닙니다.
import re

def initial():
    return {"words": []}

def view(state):
    top = sorted(set(state["words"]), key=lambda w: -len(w))[:3]
    return vstack([text(f"{len(state['words']):,} words", mono=True)]
                  + [row(w, detail=f"{len(w)}") for w in top])

def on_open(state):
    state["words"] = re.findall(r"\w+", context()["before"])
    return state
쓴 글을 읽어 세어 보고, 가장 긴 낱말을 늘어놓는 패널입니다.

PyMini 1.0

첫 번째 버전입니다. 액션은 문자열 하나를 다른 문자열로 바꾸었고, 패널은 버튼 몇 개로 정해진 글자를 입력하거나 값을 설정하는 정도였습니다.

예시바뀐 점
def transform(text):int, float, str, bool, None, list, dict와 if, while, for, 그리고 기본값과 키워드 인수를 받는 def.
f"{n} words"서식 지정 없는 f-문자열, 음수 인덱스를 쓰는 인덱싱과 슬라이싱.
sorted(text.split())내장 함수 24개와 str, list, dict의 흔히 쓰는 메서드.
button("+", set={"count": 1})텍스트, 버튼, 입력란, 스택, 그리드. 버튼은 글자를 입력하거나 상태를 바꾸는 것밖에 못 했습니다.
while True:import는 전혀 없었고, 모든 스크립트에 단계 예산이 있었으며, 문자열과 리스트가 커질 수 있는 크기에도 상한이 있었습니다.

다음에 볼 것

언어의 전체 모습, 패널의 어휘, 그리고 한 줄도 치지 않고 둘 다 써 주는 빌더.

동작 ›패널 ›플러그인 ›크리에이터 ›
App Store에서 다운로드