[{"content":"nginx配置文件位于/etc/nginx目录，其中核心配置是/etc/nginx/nginx.conf，一般默认加载/etc/nginx/sites-enabled/目录中的站点配置。\n站点配置文件内容可参考以下内容。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 # 站点配置: example.com # 站点证书 example.com *.example.com ssl_certificate /path/to/cert/example.com/cert.pem; ssl_certificate_key /path/to/cert/example.com/key.pem; # 支持常规连接和WebSocket连接 map $http_connection $connection_upgrade { \u0026#34;~*Upgrade\u0026#34; $http_connection; default keep-alive; } # HTTP服务 重定向到HTTPS server { listen 80; listen [::]:80; server_name example.com *.example.com; return 301 https://$server_name$request_uri; } # example.com 站点服务 server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name example.com; # 默认跳转到博客站(也可以直接跳转) location / { #add_header Content-Type \u0026#39;text/plain; charset=utf-8\u0026#39;; #return 200 \u0026#39;maybe you are lost, please visit: https://blog.example.com ?\u0026#39;; return 302 https://example.com/blog; } location /blog { return 301 https://blog.example.com; } # 反向代理内部业务(仅接口代理) location /service-name/ { proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; # 配置方式与实际转发效果如下: # 带后缀`/`: https://exmaple.com/service-name/abc/def?g=123\u0026amp;h=456 -\u0026gt; http://127.0.0.1:12345/abc/def?g=123\u0026amp;h=456 # 无后缀`/`: https://exmaple.com/service-name/abc/def?g=123\u0026amp;h=456 -\u0026gt; http://127.0.0.1:12345/service-name/abc/def?g=123\u0026amp;h=456 proxy_pass http://127.0.0.1:12345/; } } # ws.example.com 站点服务 server { listen 443 ssl; listen [::]:443 ssl; server_name ws.example.com; location / { add_header Content-Type \u0026#39;text/plain; charset=utf-8\u0026#39;; return 200 \u0026#39;hello, this is ws.example.com\u0026#39;; } # 反向代理内部业务(接口+WebSocket) location /ws-path/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; proxy_pass http://127.0.0.1:12345; } } # grpc.example.com 站点服务 server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name grpc.example.com; location / { add_header Content-Type \u0026#39;text/plain; charset=utf-8\u0026#39;; return 200 \u0026#39;hello, this is grpc.example.com\u0026#39;; } # 反向代理内部业务(gRPC) location /\u0026lt;grpc-service-name\u0026gt;/ { grpc_pass grpc://127.0.0.1:12345; } } # blog.example.com 站点服务 server { listen 443 ssl; listen [::]:443 ssl; server_name blog.example.com; root /path/to/blog/root; location / { index index.html; } } # pxy.example.com 站点服务 server { listen 443 ssl; listen [::]:443 ssl; server_name pxy.example.com; location / { proxy_redirect off; proxy_ssl_server_name on; proxy_pass https://jmsnet; } } # *.example.com 站点服务 server { listen 443 ssl; listen [::]:443 ssl; server_name *.example.com; error_page 404 = /error; location / { add_header Content-Type \u0026#39;text/plain; charset=utf-8\u0026#39;; return 200 \u0026#39;hello, goodbye\u0026#39;; } location /error { add_header Content-Type \u0026#39;text/plain; charset=utf-8\u0026#39;; return 404 \u0026#39;maybe you are lost\u0026#39;; } } ","date":"2025-12-23T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/snippet/nginx-config/","title":"nginx配置参考"},{"content":"数据链路 大致数据链路如下图所示，基本思路是客户端经两级跳转访问服务端。\nsequenceDiagram Client-\u0026gt;\u0026gt;In: 1. 客户端请求代理入口 In-\u0026gt;\u0026gt;Out: 2. 代理入口请求代理出口 Out-\u0026gt;\u0026gt;Server: 3. 代理出口访问目标服务 Server-\u0026gt;\u0026gt;Out: 4. 目标服务回复代理出口 Out-\u0026gt;\u0026gt;In: 5. 代理出口回复代理入口 In-\u0026gt;\u0026gt;Client: 6. 代理入口回复客户端 具备以下特点：\n完整链路信息只有客户端得知，客户端看起来只是在访问In 只有In知道在访问Out，也只知道在访问Out，不知道实际目标 服务端只知道请求来自Out，不知道客户端存在 客户端配置 参考文档: dialer-proxy\n代理节点部分核心配置如下:\n1 2 3 4 5 6 7 8 9 proxies: - name: in # 代理入口，常规配置 type: ... - name: out # 代理出口，需要声明代理入口 dialer-proxy: in type: ... rules: - MATCH,out # 此处选择`out`节点 代理组部分核心配置如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 proxy-groups: - name: PROXY # 默认代理策略，可以SAFE优先，也可以原生优先，推荐保证可用 type: select proxies: - PROXY-SAFE - RAW - name: PROXY-CHEAP # 低计费策略，优先低倍率节点，推荐保证可用 type: select / url-test / fallback proxies: - \u0026lt;node-name\u0026gt; - RAW - name: PROXY-SAFE # 稳定出口策略，不推荐自动回落 type: select proxies: - \u0026lt;node-name\u0026gt; - name: dialer-in # 代理入口组，推荐保证可用且整体低延迟 type: select / url-test / fallback hidden: true # 可以隐藏 proxies: - \u0026lt;node-name\u0026gt; - name: RAW # 原生节点组 type: select proxies: - \u0026lt;node-name\u0026gt; 服务端配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 代理服务端，以直连模式运行 mode: direct listeners: # 服务协议：VMess # 传输方式：WebSocket - name: vmess-ws\u0026amp;grpc type: vmess # 仅允许本机(反向代理)访问 listen: 127.0.0.1 # 服务端口 port: 10800 users: - username: 1 uuid: \u0026lt;uuid\u0026gt; alterId: 0 # WebSocket路径 ws-path: /\u0026lt;websocket-path\u0026gt;/ # gRPC服务名称 grpc-service-name: \u0026lt;grpc-server-name\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 # SSL证书配置 ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/cert.key; map $http_connection $connection_upgrade { \u0026#34;~*Upgrade\u0026#34; $http_connection; default keep-alive; } server { # 监听443端口，启用SSL加密，启用HTTP2(以支持gRPC) listen 443 ssl; listen [::]:443 ssl; http2 on; # 服务器名称 server_name \u0026lt;server-name\u0026gt;; # 常规站点配置 location / { add_header Content-Type \u0026#39;text/plain; charset=utf-8\u0026#39;; return 200 \u0026#39;hello, this is my site\u0026#39;; } # 反向代理WebSocket location /\u0026lt;websocket-path\u0026gt;/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; proxy_pass http://127.0.0.1:10800; } # 反向代理gRPC location /\u0026lt;grpc-server-name\u0026gt;/ { grpc_pass grpc://127.0.0.1:10800; } } 其他配置(客户端规则) 路由规则如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 rules: # 拦截广告规则 - RULE-SET,reject,REJECT # 直连私网、境内服务等 - RULE-SET,private,DIRECT - RULE-SET,icloud,DIRECT - RULE-SET,apple,DIRECT - RULE-SET,direct,DIRECT - RULE-SET,lancidr,DIRECT - RULE-SET,cncidr,DIRECT - DOMAIN-SUFFIX,cn,DIRECT - DOMAIN-KEYWORD,-cn,DIRECT - GEOSITE,CN,DIRECT - GEOIP,LAN,DIRECT - GEOIP,CN,DIRECT # 本地共享、大流量传输优先低计费节点 - RULE-SET,share,PROXY-CHEAP - RULE-SET,download,PROXY-CHEAP # 确保稳定出口的服务优先SAFE节点 - RULE-SET,safe,PROXY-SAFE - RULE-SET,telegramcidr,PROXY-SAFE # 已知服务和无匹配默认代理访问 - RULE-SET,gfw,PROXY - RULE-SET,proxy,PROXY - MATCH,PROXY 路由规则集合如下所示: 在线规则集合参考项目: Loyalsoldier/clash-rules\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 rule-providers: # 大流量传输规则组 download: type: inline behavior: classical payload: # huggingface model - DOMAIN-REGEX,^cdn-lfs.*.hf.co # dockerhub image - DOMAIN,production.cloudflare.docker.com # github image - DOMAIN,ghcr.io - DOMAIN,pkg-containers.githubusercontent.com # github storage - DOMAIN,objects.githubusercontent.com - DOMAIN,release-assets.githubusercontent.com # cloudflare R2 storage - DOMAIN-SUFFIX,r2.cloudflarestorage.com # sourceforge - DOMAIN-SUFFIX,dl.sourceforge.net # google drive - DOMAIN,drive.usercontent.google.com # youtube video - DOMAIN-SUFFIX,googlevideo.com # 确保出口稳定规则组 safe: type: inline behavior: classical payload: - DOMAIN-SUFFIX,chatgpt.com - DOMAIN-SUFFIX,docker.com - DOMAIN-SUFFIX,docker.io - DOMAIN-SUFFIX,openai.com - DOMAIN-SUFFIX,redd.it - DOMAIN-SUFFIX,reddit.com - DOMAIN-SUFFIX,twimage.com - DOMAIN-SUFFIX,twitter.com - DOMAIN-SUFFIX,x.com - DOMAIN-KEYWORD,amazon - DOMAIN-KEYWORD,github - DOMAIN-KEYWORD,gmail - DOMAIN-KEYWORD,google - DOMAIN-KEYWORD,openai - DOMAIN-KEYWORD,reddit - DOMAIN-KEYWORD,youtube share: type: inline behavior: classical payload: - SRC-IP-CIDR,192.168.0.0/16 - SRC-IP-CIDR,172.16.0.0/12 reject: type: http behavior: domain url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/reject.txt path: ./ruleset/reject.yaml interval: 86400 icloud: type: http behavior: domain url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/icloud.txt path: ./ruleset/icloud.yaml interval: 86400 apple: type: http behavior: domain url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/apple.txt path: ./ruleset/apple.yaml interval: 86400 proxy: type: http behavior: domain url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/proxy.txt path: ./ruleset/proxy.yaml interval: 86400 direct: type: http behavior: domain url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/direct.txt path: ./ruleset/direct.yaml interval: 86400 private: type: http behavior: domain url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/private.txt path: ./ruleset/private.yaml interval: 86400 gfw: type: http behavior: domain url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/gfw.txt path: ./ruleset/gfw.yaml interval: 86400 tld-not-cn: type: http behavior: domain url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/tld-not-cn.txt path: ./ruleset/tld-not-cn.yaml interval: 86400 telegramcidr: type: http behavior: ipcidr url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/telegramcidr.txt path: ./ruleset/telegramcidr.yaml interval: 86400 cncidr: type: http behavior: ipcidr url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/cncidr.txt path: ./ruleset/cncidr.yaml interval: 86400 lancidr: type: http behavior: ipcidr url: https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/lancidr.txt path: ./ruleset/lancidr.yaml interval: 86400 ","date":"2025-12-12T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/snippet/clash-config/","title":"代理配置"},{"content":"Debian某些版本默认的网络/防火墙规则管理组件，此处简要记录配置操作。\n规则配置文件一般位于/etc/nftables.conf，样例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #!/usr/sbin/nft -f # 清空现有规则 flush ruleset table inet filter { chain input { type filter hook input priority filter; # 允许本机连接 iifname lo accept # 允许已建立和关联的入站连接 ct state established,related accept # 允许ICMP协议请求入站，频率上限每秒一次 ip protocol icmp limit rate 1/second accept # 允许指定端口入站 tcp dport {22,80,443} accept # 允许指定协议入站 tcp dport {ssh,http,https} accept # 丢弃其他入站 drop } chain forward { type filter hook forward priority filter; # 丢弃转发流量 drop } chain output { type filter hook output priority filter; # 放行出站连接 accept } } 相关操作命令\n1 2 3 4 5 6 7 8 9 10 11 12 # 校验配置文件 sudo nft -c -f /etc/nftables.conf # 加载配置文件 sudo nft -f /etc/nftables.conf # 检查规则生效情况 sudo nft list ruleset # 服务启动\u0026amp;配置自启 sudo systemctl start nftables sudo systemctl enable nftables ","date":"2025-12-10T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/snippet/nftables-config/","title":"nftables网络配置"},{"content":" 脚本应当在Bash环境中执行，且必须部署faketime、openssl命令行工具\n脚本参数\nCERT_HOST 颁发证书的目标地址/名称，支持多个值，空格分隔即可 localhost 127.0.0.1 ::1 STRATEGY 本机证书校验及颁发策略，对主机自身的地址及名称的处理原则，不涉及CERT_HOST变量(变量中指定的地址及名称保证进行校验及颁发)。策略支持以下类型： BASIC: 验证当前证书对localhost/127.0.0.1颁发即可,若验证不通过,颁发localhost/127.0.0.1的证书(构建镜像时默认使用该配置) NORMAL: 验证当前证书对localhost/127.0.0.1颁发即可,若验证不通过,颁发\u0026lt;hostname\u0026gt;/\u0026lt;hostip\u0026gt;/localhost/127.0.0.1的证书(启动时建议使用该配置) STRICT: 验证当前证书对\u0026lt;hostname\u0026gt;/\u0026lt;hostip\u0026gt;/localhost/127.0.0.1颁发,若验证不通过,颁发\u0026lt;hostname\u0026gt;/\u0026lt;hostip\u0026gt;/localhost/127.0.0.1的证书 2025-12-15: 已颁发证书校验部分更新为openssl verify命令，支持泛域名，支持各种IPv6格式，修复基于文本匹配的校验不严谨问题\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 #!/bin/bash ## 不容忍错误(任何命令出错都退出脚本) set -e ########################## [打印日志] ########################## ## 日志记录的模块名称 MODULE_NAME=cert-util ## ANSI字符颜色转义 RED=\u0026#39;\\033[1;31m\u0026#39; GREEN=\u0026#39;\\033[1;32m\u0026#39; YELLOW=\u0026#39;\\033[1;33m\u0026#39; ## 重置所有字符格式 RESET=\u0026#39;\\033[0m\u0026#39; ## 日志记录方法组 function log() { level=$1 format=$2 message=${@:3} time=$(date \u0026#34;+%F %T\u0026#34;) echo -e \u0026#34;$format[$time][$MODULE_NAME] $level - $message$RESET\u0026#34; } function success() { log \u0026#34;SUCCESS\u0026#34; $GREEN $*; } function info() { log \u0026#34;INFO \u0026#34; $RESET $*; } function warn() { log \u0026#34;WARN \u0026#34; $YELLOW $*; } function error() { log \u0026#34;ERROR \u0026#34; $RED $*; } ################################################################# if [[ -z \u0026#34;$STRATEGY\u0026#34; ]]; then warn \u0026#34;未设置证书颁发策略类型,指定为默认值[NORMAL]\u0026#34; STRATEGY=NORMAL fi case \u0026#34;$STRATEGY\u0026#34; in \u0026#34;BASIC\u0026#34;) info \u0026#34;证书颁发策略类型为[$STRATEGY],仅验证/颁发localhost/127.0.0.1\u0026#34; ;; \u0026#34;NORMAL\u0026#34;) info \u0026#34;证书颁发策略类型为[$STRATEGY],将验证localhost/127.0.0.1,颁发hostname/hostip/localhost/127.0.0.1\u0026#34; ;; \u0026#34;STRICT\u0026#34;) info \u0026#34;证书颁发策略类型为[$STRATEGY],将验证/颁发本机hostname/hostip/localhost/127.0.0.1\u0026#34; ;; *) error \u0026#34;不支持的证书颁发策略类型[$STRATEGY],退出脚本\u0026#34; exit 1 ;; esac ## 脚本所在目录 SCRIPT_DIR=$( cd -- \u0026#34;$( dirname -- \u0026#34;${BASH_SOURCE[0]}\u0026#34; )\u0026#34; \u0026amp;\u0026gt; /dev/null \u0026amp;\u0026amp; pwd ) ## 是否需要颁发证书 ISSUE_CERT=FALSE ## TODO: 在此处指定CA证书 CA_CERT_ROOT=\u0026#34;$SCRIPT_DIR/ca-cert\u0026#34; ## 证书颁发机构(此处推荐使用Server CA, 信任链大致为: 根证书(Root CA) -\u0026gt; 服务器中间证书(Server CA) -\u0026gt; 服务器证书(待颁发)) CA_CRT=\u0026#34;$CA_CERT_ROOT/???.crt\u0026#34; CA_KEY=\u0026#34;$CA_CERT_ROOT/???.key\u0026#34; CA_KEY_PASS=\u0026#34;$CA_CERT_ROOT/???.key.pass\u0026#34; ## 待颁发的证书 SERVER_CERT_ROOT=\u0026#34;$SCRIPT_DIR/server-cert\u0026#34; SERVER_CERT_CSR_CONF=\u0026#34;$SERVER_CERT_ROOT/server.conf\u0026#34; # 证书颁发请求配置 SERVER_CERT_CSR=\u0026#34;$SERVER_CERT_ROOT/server.csr\u0026#34; # 证书颁发请求 SERVER_CERT_CRT=\u0026#34;$SERVER_CERT_ROOT/server.crt\u0026#34; # 证书(通常是PEM格式) SERVER_CERT_KEY=\u0026#34;$SERVER_CERT_ROOT/server.key\u0026#34; # 证书私钥 SERVER_CERT_CHAIN=\u0026#34;$SERVER_CERT_ROOT/fullchain.crt\u0026#34; # 证书链(通常是PEM格式) SERVER_CERT_P12=\u0026#34;$SERVER_CERT_ROOT/server.p12\u0026#34; # 证书(PKCS12格式) SERVER_CERT_P12_PASS=\u0026#34;$SERVER_CERT_ROOT/server.p12.pass\u0026#34; # 证书密钥(仅适用于PKCS12格式) SERVER_CERT_COMMON_NAME={$COMMON_NAME:-localhost} ## 校验现有证书有效性(是否对给定的名称/地址有效;是否处于有效期) if [[ -f \u0026#34;$SERVER_CERT_CRT\u0026#34; ]]; then if [[ -n \u0026#34;$CERT_HOST\u0026#34; ]]; then info \u0026#34;已设置环境变量[CERT_HOST]=[$CERT_HOST],将逐个验证是否已颁发证书\u0026#34; for host in $CERT_HOST; do if [[ $host =~ ^([0-9]{1,3}\\.){3}[0-9]{1,3}$ || $host == *:* ]]; then # 简单的IPv4正则，将4组最长3位的数字视为IPv4地址 # 包含冒号视为IPv6地址 if openssl verify -verify_ip \u0026#34;$host\u0026#34; \u0026#34;$SERVER_CERT_CRT\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then success \u0026#34;[√]已颁发地址证书[$host]\u0026#34; else error \u0026#34;[×]未颁发地址证书[$host]\u0026#34; ISSUE_CERT=TRUE fi else # 其余的视为名称/域名 if openssl verify -verify_hostname \u0026#34;$host\u0026#34; \u0026#34;$SERVER_CERT_CRT\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then success \u0026#34;[√]已颁发名称证书[$host]\u0026#34; else error \u0026#34;[×]未颁发名称证书[$host]\u0026#34; ISSUE_CERT=TRUE fi fi done else info \u0026#34;未设置环境变量[CERT_HOST],跳过证书颁发目标验证\u0026#34; fi ## 创建\u0026amp;启动容器时通常是随机主机名及地址，构建时基本不可能完整颁发主机证书，只能颁发出localhost/127.0.0.1 info \u0026#34;验证本机名称/IP是否已颁发证书\u0026#34; if [[ \u0026#34;$STRATEGY\u0026#34; = \u0026#34;STRICT\u0026#34; ]]; then info \u0026#34;当前策略类型为[$STRATEGY],将验证本机hostname/hostip/localhost/127.0.0.1\u0026#34; LOCALHOST=\u0026#34;$(hostname -f) localhost $(hostname -I) 127.0.0.1\u0026#34; else info \u0026#34;当前策略类型为[$STRATEGY],仅验证localhost/127.0.0.1\u0026#34; LOCALHOST=\u0026#34;localhost 127.0.0.1\u0026#34; fi for host in $LOCALHOST; do if [[ $host =~ ^([0-9]{1,3}\\.){3}[0-9]{1,3}$ || $host == *:* ]]; then # 简单的IPv4正则，将4组最长3位的数字视为IPv4地址 # 包含冒号视为IPv6地址 if openssl verify -verify_ip \u0026#34;$host\u0026#34; \u0026#34;$SERVER_CERT_CRT\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then success \u0026#34;[√]已颁发地址证书[$host]\u0026#34; else error \u0026#34;[×]未颁发地址证书[$host]\u0026#34; ISSUE_CERT=TRUE fi else # 其余的视为名称/域名 if openssl verify -verify_hostname \u0026#34;$host\u0026#34; \u0026#34;$SERVER_CERT_CRT\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then success \u0026#34;[√]已颁发名称证书[$host]\u0026#34; else error \u0026#34;[×]未颁发名称证书[$host]\u0026#34; ISSUE_CERT=TRUE fi fi done info \u0026#34;证书有效期范围[$(date -d \u0026#34;$(openssl x509 -in \u0026#34;$SERVER_CERT_CRT\u0026#34; -noout -startdate | cut -d= -f2)\u0026#34; +\u0026#39;%F %T\u0026#39;), $(date -d \u0026#34;$(openssl x509 -in \u0026#34;$SERVER_CERT_CRT\u0026#34; -noout -enddate | cut -d= -f2)\u0026#34; +\u0026#39;%F %T\u0026#39;)]\u0026#34; START=$(date -d \u0026#34;$(openssl x509 -in \u0026#34;$SERVER_CERT_CRT\u0026#34; -noout -startdate | cut -d= -f2)\u0026#34; +%s) END=$(date -d \u0026#34;$(openssl x509 -in \u0026#34;$SERVER_CERT_CRT\u0026#34; -noout -enddate | cut -d= -f2)\u0026#34; +%s) NOW=$(date -u +%s) if [[ \u0026#34;$START\u0026#34; -le \u0026#34;$(date -u +%s)\u0026#34; \u0026amp;\u0026amp; \u0026#34;$(date -d \u0026#39;+1 month\u0026#39; -u +%s)\u0026#34; -le \u0026#34;$END\u0026#34; ]]; then success \u0026#34;当前时间(及一个月内)都处于证书有效时间范围\u0026#34; else error \u0026#34;当前时间及一个月内将不在证书有效时间范围\u0026#34; ISSUE_CERT=TRUE fi else warn \u0026#34;证书文件[$SERVER_CERT_CRT]不存在,将颁发证书\u0026#34; ISSUE_CERT=TRUE fi if [[ $ISSUE_CERT = \u0026#34;FALSE\u0026#34; ]]; then success \u0026#34;默认证书可用,无需重新颁发\u0026#34; exit 0 fi if [[ -d \u0026#34;$SERVER_CERT_ROOT\u0026#34; ]]; then SERVER_CERT_ROOT_BAK=\u0026#34;${SERVER_CERT_ROOT%/}-bak-$(date +%Y%m%d%H%M%S)\u0026#34; warn \u0026#34;重新颁发证书,现有证书[$SERVER_CERT_ROOT]将备份为[$SERVER_CERT_ROOT_BAK]\u0026#34; mv \u0026#34;$SERVER_CERT_ROOT\u0026#34; \u0026#34;$SERVER_CERT_ROOT_BAK\u0026#34; fi info \u0026#34;证书相关文件将保存到[$SERVER_CERT_ROOT]目录\u0026#34; mkdir -p \u0026#34;$SERVER_CERT_ROOT\u0026#34; ## 明确指定证书有效期需要ca相关配置,比较麻烦,因此下面用faketime做了简易版实现 ## 证书有效期：本月第一天凌晨开始，十年内有效 # CERT_START=$(date +\u0026#34;%Y%m01000000Z\u0026#34;) # CERT_END=$(date -d \u0026#34;$(date +%Y-%m-01) +10 years\u0026#34; +\u0026#34;%Y%m%d235959Z\u0026#34;) ## 快捷颁发证书(仅对CN(Common Name)颁发证书,无法颁发SAN(Subject Alt Name,多个IP及域名),目前浏览器优先验证SAN,CN作为fallback) # SUBJECT=\u0026#34;/C=CN/ST=Shandong/L=Qingdao/O=Shandong R\u0026amp;D Center/CN=host.domain\u0026#34; # openssl req -new -nodes -newkey rsa:2048 -keyout \u0026#34;$SERVER_CERT_KEY\u0026#34; -out \u0026#34;$SERVER_CERT_CSR\u0026#34; -subj \u0026#34;$SUBJECT\u0026#34; # openssl x509 -req -in \u0026#34;$SERVER_CERT_CSR\u0026#34; -out \u0026#34;$SERVER_CERT_CRT\u0026#34; -outform PEM -CA \u0026#34;$CA_CRT\u0026#34; -CAkey \u0026#34;$CA_KEY\u0026#34; -passin \u0026#34;file:$CA_KEY_PASS\u0026#34; -CAcreateserial -days 3650 -sha256 ## 生成配置文件,用于生成CSR(证书颁发请求文件) ## 注: 文件内容没有缩进是特意的，避免程序读取出现问题，也避免用sed消除行前空白(最后的EOF前不允许有空白，否则后续命令也被视为文本内容) cat \u0026lt;\u0026lt;EOF \u0026gt; \u0026#34;$SERVER_CERT_CSR_CONF\u0026#34; [req] default_bits = 2048 default_md = sha256 encrypt_key = no prompt = no distinguished_name = dn req_extensions = v3_req [dn] C = CN ST = Shandong L = Qingdao O = Shandong R\u0026amp;D Center OU = Qingdao R\u0026amp;D Center CN = $SERVER_CERT_COMMON_NAME [v3_req] keyUsage = critical, digitalSignature, keyEncipherment, dataEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] # DNS.1 = localhost # IP.1 = 127.0.0.1 # IP.2 = ::1 EOF if [[ -n \u0026#34;$CERT_HOST\u0026#34; ]]; then info \u0026#34;已设置变量[CERT_HOST]=[$CERT_HOST],将颁发证书(配置为CSR中的SAN)\u0026#34; else CERT_HOST=\u0026#34;\u0026#34; warn \u0026#34;未设置变量[CERT_HOST],将仅为默认名称/IP颁发证书\u0026#34; fi if [[ \u0026#34;$STRATEGY\u0026#34; = \u0026#34;BASIC\u0026#34; ]]; then info \u0026#34;当前策略类型为[$STRATEGY],仅为默认本机名称/IP(localhost/127.0.0.1/::1)颁发证书\u0026#34; HOSTS=\u0026#34;localhost 127.0.0.1 ::1 $CERT_HOST\u0026#34; else info \u0026#34;当前策略类型为[$STRATEGY],将为本机名称/IP(\u0026lt;hostname\u0026gt;/\u0026lt;host-ip\u0026gt;/localhost/127.0.0.1/::1)颁发证书\u0026#34; HOSTS=\u0026#34;$(hostname -f) localhost $(hostname -I) 127.0.0.1 ::1 $CERT_HOST\u0026#34; fi HOSTS=$(echo \u0026#34;$HOSTS\u0026#34; | xargs | tr \u0026#34; \u0026#34; \u0026#34;\\n\u0026#34; | sort -u | tr \u0026#34;\\n\u0026#34; \u0026#34; \u0026#34;) info \u0026#34;待颁发证书的名称/IP(去重+排序): $HOSTS\u0026#34; NAMES=\u0026#34;\u0026#34; IPS=\u0026#34;\u0026#34; info \u0026#34;基于简易规则快速判断目标类型(IPv4/IPv6/Name)\u0026#34; for host in $HOSTS; do if [[ $host =~ ^([0-9]{1,3}\\.){3}[0-9]{1,3}$ ]]; then # 简单的IPv4正则，将4组最长3位的数字视为IPv4地址 info \u0026#34; \u0026gt;\u0026gt; IPv4: $host\u0026#34; IPS=\u0026#34;$host $IPS\u0026#34; elif [[ $host == *:* ]]; then # 包含冒号视为IPv6地址 info \u0026#34; \u0026gt;\u0026gt; IPv6: $host\u0026#34; IPS=\u0026#34;$host $IPS\u0026#34; else # 其余的视为名称/域名 info \u0026#34; \u0026gt;\u0026gt; Name: $host\u0026#34; NAMES=\u0026#34;$host $NAMES\u0026#34; fi done info \u0026#34;将为以下名称颁发证书(配置为CSR中的SAN): $NAMES\u0026#34; i=1 for name in $NAMES; do info \u0026#34;DNS.$i = $name \u0026gt;\u0026gt; $SERVER_CERT_CSR_CONF\u0026#34; echo \u0026#34;DNS.$i = $name\u0026#34; \u0026gt;\u0026gt; \u0026#34;$SERVER_CERT_CSR_CONF\u0026#34; i=$((i+1)) done info \u0026#34;将为以下IP颁发证书(配置为CSR中的SAN): $IPS\u0026#34; i=1 for ip in $IPS; do info \u0026#34;IP.$i = $ip \u0026gt;\u0026gt; $SERVER_CERT_CSR_CONF\u0026#34; echo \u0026#34;IP.$i = $ip\u0026#34; \u0026gt;\u0026gt; \u0026#34;$SERVER_CERT_CSR_CONF\u0026#34; i=$((i+1)) done info \u0026#34;生成CSR(证书颁发请求)\u0026#34; openssl req -new -config \u0026#34;$SERVER_CERT_CSR_CONF\u0026#34; -keyout \u0026#34;$SERVER_CERT_KEY\u0026#34; -out \u0026#34;$SERVER_CERT_CSR\u0026#34; if command -v faketime \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then if [[ \u0026#34;$(date +%d)\u0026#34; = \u0026#34;01\u0026#34; ]]; then CERT_START=\u0026#34;$(date -d \u0026#39;-1 month\u0026#39; +%Y-%m-01) 00:00:00\u0026#34; warn \u0026#34;当前日期是[$(date +%Y-%m-%d)],为避免时区问题导致证书无效,生效开始时间设为上个月[$(date -d \u0026#39;-1 month\u0026#39; +%Y-%m-%d)]\u0026#34; else CERT_START=\u0026#34;$(date +%Y-%m-01) 00:00:00\u0026#34; fi info \u0026#34;颁发证书，自[$CERT_START(UTC)]起生效，有效期十年(用faketime控制有效期,启用X509v3扩展以支持SAN)\u0026#34; TZ=UTC faketime \u0026#34;$CERT_START\u0026#34; openssl x509 -req -in \u0026#34;$SERVER_CERT_CSR\u0026#34; -extfile \u0026#34;$SERVER_CERT_CSR_CONF\u0026#34; -extensions \u0026#34;v3_req\u0026#34; -out \u0026#34;$SERVER_CERT_CRT\u0026#34; -outform PEM -CA \u0026#34;$CA_CRT\u0026#34; -CAkey \u0026#34;$CA_KEY\u0026#34; -passin \u0026#34;file:$CA_KEY_PASS\u0026#34; -CAcreateserial -days 3650 -sha256 else info \u0026#34;颁发证书，立即生效，有效期十年(启用X509v3扩展以支持SAN)\u0026#34; openssl x509 -req -in \u0026#34;$SERVER_CERT_CSR\u0026#34; -extfile \u0026#34;$SERVER_CERT_CSR_CONF\u0026#34; -extensions \u0026#34;v3_req\u0026#34; -out \u0026#34;$SERVER_CERT_CRT\u0026#34; -outform PEM -CA \u0026#34;$CA_CRT\u0026#34; -CAkey \u0026#34;$CA_KEY\u0026#34; -passin \u0026#34;file:$CA_KEY_PASS\u0026#34; -CAcreateserial -days 3650 -sha256 fi success \u0026#34;证书颁发完成,证书信息如下:\u0026#34; openssl x509 -in \u0026#34;$SERVER_CERT_CRT\u0026#34; -noout -text info \u0026#34;生成证书链\u0026#34; cat \u0026#34;$SERVER_CERT_CRT\u0026#34; \u0026#34;$CA_CRT\u0026#34; \u0026gt; \u0026#34;$SERVER_CERT_CHAIN\u0026#34; info \u0026#34;生成证书密钥(PKCS12)\u0026#34; cat /proc/sys/kernel/random/uuid \u0026gt; $SERVER_CERT_P12_PASS info \u0026#34;生成证书(PKCS12)\u0026#34; openssl pkcs12 -export -in \u0026#34;$SERVER_CERT_CHAIN\u0026#34; -inkey \u0026#34;$SERVER_CERT_KEY\u0026#34; -out \u0026#34;$SERVER_CERT_P12\u0026#34; -name \u0026#34;$SERVER_CERT_COMMON_NAME\u0026#34; -password \u0026#34;file:$SERVER_CERT_P12_PASS\u0026#34; success \u0026#34;证书(PEM): $SERVER_CERT_CRT\u0026#34; success \u0026#34;私钥: $SERVER_CERT_KEY\u0026#34; success \u0026#34;证书链(PEM): $SERVER_CERT_CHAIN\u0026#34; ","date":"2025-08-21T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/snippet/issue-cert/","title":"证书颁发脚本"},{"content":"泛型 泛型(Generic)是非常重要的语言功能，广泛应用于各种项目、工具、框架。\n本文主要关注泛型的应用，由浅入深介绍和讨论能力和限制，给出相关示例来演示应用。\n演示代码语言/平台采用最新LTS或主流支持版本，详细情况如下：\nC# 12 on .NET 8 (LTS) F# 8 on .NET 8 (LTS) Java 21 on JDK 21 (LTS) Scala 3.3.x (LTS) on JDK 21 (LTS) Scala 2.13.x on JDK 21 (LTS) Python 3.13.x .NET版本支持策略参考官方文档: .NET Support Policy; C#和F#随.NET一同发布/更新\nScala与JDK兼容情况可参考官方介绍: JDK Compatibility\nScala 2.x 维护计划在Scala 2 maintenance plans及Scala development guarantees中有介绍，简言之2.13将持续维护，2.12将在sbt 1广泛应用期间持续维护 Scala 2.13相对与2.12的变化可参考: Migrating a Project to Scala 2.13\u0026rsquo;s Collections, Scala 2.13.0 is now available!, Releases / Scala 2.13.0 Scala 3相对于2的变化可参考: Scala 3 Migration Guide\nPython的版本支持情况可参考官方介绍: Status of Python versions\n基本语法 本节简要介绍泛型在各语言中的基本使用。\nC# 官方文档: \\\nGenerics in .NET C# specifications / Types / Constructed types Generic classes and methods \u0026hellip;\u0026hellip; 泛型接口、泛型类和泛型方法 如下代码演示了泛型接口、泛型类、泛型方法(实例方法及静态方法)的定义和调用。\\\nGeneric Classes (C# Programming Guide) Generic Interfaces (C# Programming Guide) Generic methods (C# programming guide) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 // ==========[定义泛型]========== // 定义泛型接口 interface IWritable\u0026lt;T\u0026gt; { void Write(T value); } interface IReadable\u0026lt;T\u0026gt; { T Read(); // 接口自带默认实现 string ReadString() =\u0026gt; Read().ToString(); } // 定义泛型类(并实现泛型接口) class Response\u0026lt;T\u0026gt; : IReadable\u0026lt;T\u0026gt;, IWritable\u0026lt;T\u0026gt; { public int Code { get; set; } public string Message { get; set; } // Data属性是个泛型属性 public T Data { get; set; } // 构造器方法 public Response() { } public Response(int code, string message, T data) : this() { this.Code = code; this.Message = message; this.Data = data; } // 解构方法 public void Deconstruct(out int code, out string message, out T data) { code = Code; message = Message; data = Data; } // 实现泛型接口中定义的方法 public T Read() =\u0026gt; Data; public void Write(T value) =\u0026gt; Data = value; // 定义泛型实例方法 public bool TryRefreshData\u0026lt;U\u0026gt;(U data) { if (data is T t) { this.Data = t; return true; } return false; } // 尽量避免在泛型类中定义静态方法，调用相对比较麻烦 // 定义泛型静态方法 public static Response\u0026lt;U\u0026gt; Success\u0026lt;U\u0026gt;(U data) =\u0026gt; new(0, \u0026#34;success\u0026#34;, data); // 定义静态方法 public static Response\u0026lt;string\u0026gt; Failed(string message) =\u0026gt; new(1, message, \u0026#34;failed\u0026#34;); } // 定义工具类 // C#中，泛型静态方法最好放在单独的工具类中，可以直接以 class ResponseHelper { // 定义泛型静态方法 public static Response\u0026lt;T\u0026gt; Success\u0026lt;T\u0026gt;(T data) =\u0026gt; new(0, \u0026#34;success\u0026#34;, data); public static Response\u0026lt;string\u0026gt; Failed(string message) =\u0026gt; new(1, message, \u0026#34;failed\u0026#34;); } // 定义(静态)扩展类 static class ResponseExtension { // 定义泛型扩展方法 public static Response\u0026lt;T\u0026gt; Copy\u0026lt;T\u0026gt;(this Response\u0026lt;T\u0026gt; response) =\u0026gt; new(response.Code, response.Message, response.Data); // 定义泛型扩展方法 public static bool IsSuccess\u0026lt;T\u0026gt;(this Response\u0026lt;T\u0026gt; response) =\u0026gt; response.Code == 0; } // ==========[使用泛型]========== // 初始化对象(调用构造器) var response = new Response\u0026lt;string\u0026gt;(0, \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;); // 初始化对象(属性初始化) var response = new Response\u0026lt;string\u0026gt; { Code = 0, Message = \u0026#34;success\u0026#34;, Data = \u0026#34;data\u0026#34; }; // 初始化对象(new表达式) Response\u0026lt;string\u0026gt; response = new(0, \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;); // 解构对象 var (code, _, data) = response; // 泛型方法调用时，编译器可以从参数中推断类型，因此一般无需声明类型 // 调用泛型实例方法 response.TryRefreshData(123); // 调用泛型实例方法(声明类型) response.TryRefreshData\u0026lt;short\u0026gt;(123); // 调用静态泛型方法 // 调用泛型类中的静态方法时，必须要声明泛型类的具体类型(泛型静态方法的类型一般无需声明) var response = Response\u0026lt;string\u0026gt;.Success\u0026lt;short\u0026gt;(123); // 调用普通工具类中的静态方法(泛型静态方法的类型一般无需声明) var response = ResponseHelper.Success\u0026lt;short\u0026gt;(123); // 调用泛型扩展方法(与实例方法调用语法一致) var success = response.IsSuccess(); var copy = response.Copy(); 泛型接口、泛型类、泛型方法算是面向对象编程中对泛型最基本的应用。\n此外泛型抽象类并未因泛型而新增特殊之处，其本身的功能及限制只是因为它是“抽象类”而非“泛型”，因此代码中并未演示。\n泛型委托和泛型事件 委托和事件C#独有的语言功能，两者皆支持泛型，此处演示相关应用。 相关详细概念请参考官方文档。\nGeneric Delegates (C# Programming Guide) Delegates (C# Programming Guide) Events (C# Programming Guide) 1 2 3 4 5 6 // 定义泛型委托 delegate T BiOp\u0026lt;T\u0026gt;(T a, T b); // 使用泛型委托 BiOp\u0026lt;int\u0026gt; add = (a, b) =\u0026gt; a + b; var c = add(1, 2); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Singleton\u0026lt;T\u0026gt; { // 定义事件 public event EventHandler\u0026lt;T\u0026gt; ValueChanged; private T _value; public T Value { get =\u0026gt; _value; set { if (!EqualityComparer\u0026lt;T\u0026gt;.Default.Equals(Value, value)) { _value = value; OnValueChanged(value); } } } protected virtual void OnValueChanged(T value) { // 触发事件 ValueChanged?.Invoke(this, value); } } // 初始化对象 var singleton = new Singleton\u0026lt;int\u0026gt;(); // 订阅事件(多个事件处理程序) singleton.ValueChanged += (_, i) =\u0026gt; Console.WriteLine($\u0026#34;int = {i}\u0026#34;); // 赋值\u0026amp;触发事件 singleton.Value = 1; // 重复赋值\u0026amp;不触发事件 singleton.Value = 1; 事件本质上是特殊类型的委托。对两者的深入探讨请参考官网文档。、\nIntroduction to delegates Introduction to events Distinguishing Delegates and Events 泛型数组 Generics and Arrays (C# Programming Guide) 泛型数组本身没什么特殊，此处需要特别注明的是一维数组自动实现IList\u0026lt;T\u0026gt;接口，这有助于实现一个泛型方法对数组或其他集合进行遍历。\n但要注意自动实现的IList\u0026lt;T\u0026gt;接口仅支持数据读取，不能用于从数组中增删元素(会抛出异常)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 直接以集合初始化语法定义并初始化数组和List int[] array = [1, 2, 3, 4, 5]; List\u0026lt;int\u0026gt; list = [1, 2, 3, 4, 5]; // 以一致的方式访问(读取)数组和List ProcessItems(array); ProcessItems(list); static void ProcessItems\u0026lt;T\u0026gt;(IList\u0026lt;T\u0026gt; list) { // 数组对象调用IsReadOnly返回True,List对象会返回False Console.WriteLine(\u0026#34;IsReadOnly returns {0} for this collection.\u0026#34;, list.IsReadOnly); // 对数组对象调用Insert/RemoveAt方法会导致抛出异常 //list.RemoveAt(4); foreach (T item in list) { Console.Write(item?.ToString() + \u0026#34; \u0026#34;); } Console.WriteLine(); } F# TBD\nJava 泛型约束 泛型约束声明了类型参数预期的能力，声明约束后可以安全地调用支持的操作。\nC# Constraints on type parameters (C# Programming Guide) C#具备种类丰富的泛型约束，下方列表中对各种类型进行简要介绍。\n泛型约束 简介 where T : struct T必须是不可为NULL的值类型(Value Type)(可以是record struct类型)。由于所有值类型必定存在可用的无参构造器，因此这也隐含声明new()约束。struct约束不能与new()及unmanaged约束组合使用。 where T : class T必须是引用类型(Reference Type)。T可以是类、记录类(record class)、接口、委托、数组。在允许为NULL上下文(nullable context)中，T必须是不可为NULL的引用类型。 where T : class? T必须是引用类型(Reference Type)，可以是允许为NULL的引用类型，也可以是不允许为NULL的引用类型。T可以是类、记录类(record class)、接口、委托、数组。 where T : notnull T必须是不可为NULL的类型。T可以是不可为NULL的引用类型、不可为NULL的值类型。 where T : unmanaged T必须是不可为NULL的非托管类型(Unmanaged Type)。unmanaged约束隐含声明struct，不能与struct及new()约束组合使用。注：非托管类型包括内置值类型及布尔类型、枚举类型、指针类型、成员都是非托管类型的元组(tuple)和结构(struct)类型。但该约束依然不允许指针和可为NULL的非托管类型。 where T : new() T必须有公开的无参构造器。当与其他约束组合使用时，new()约束必须放在最后。new()约束不能与struct及unmanaged约束组合使用。 where T : \u0026lt;class name\u0026gt; T必须是给定的类或继承自给定的类。在允许为NULL上下文(nullable context)中，T必须是不可为NULL的引用类型。 where T : \u0026lt;class name\u0026gt;? T必须是给定的类或继承自给定的类。在允许为NULL上下文(nullable context)中，T可以是允许为NULL的引用类型，也可以是不允许为NULL的引用类型。 where T : \u0026lt;interface name\u0026gt; T必须是给定的接口或实现了给定的接口。可以声明多个接口约束；约束中给定的接口也可以是泛型接口。在允许为NULL上下文(nullable context)中，T必须是实现给定接口的不允许为NULL的类型。 where T : \u0026lt;interface name\u0026gt;? T必须是给定的接口或实现了给定的接口。可以声明多个接口约束；约束中给定的接口也可以是泛型接口。在允许为NULL上下文(nullable context)中，T必须是实现给定接口的可为NULL引用类型、不可为NULL的引用类型、值类型。T不可以是允许为NULL的值类型。 where T : U T必须是类型参数U给定的类型或继承自给定类型。在允许为NULL上下文(nullable context)中，若U是不允许为NULL的引用类型，则T也必须是不可为NULL的引用类型；若U是允许为NULL的引用类型，则不限制T是否可为NULL。 where T : default T必须未声明struct或class约束。该约束仅在显式实现接口方法或重写方法时可用，用于声明期望实现/重写的是T未被约束的方法。 where T : allows ref struct 该反约束允许T是ref struct类型。T可能是ref struct实例，泛型类型和方法必须遵循引用安全规则(ref safety rules)。C# 13.0+可用 某些约束是互斥的，某些约束必须遵循特定的顺序。\nstruct/class/class?/notnull/unmanaged约束至多允许一个，且必须是第一个约束； 基类约束(where T : Base/where T : Base?)至多允许一个，使用where T : Base?支持可为NULL的基类，基类约束不能与struct/class/class?/notnull/unmanaged约束组合使用； 不允许同时声明单个接口的可NULL和不可NULL形式(where T : I1, I1? ×; where T : I1, I2? √)； new()约束不能与struct/unmanaged约束组合使用；若存在new()约束，其必须位于约束的最后(反约束可以放在它后面)； default约束只能用于方法重写和显式实现接口方法场景，且不能与struct/class约束组合使用； 反约束allows ref struct不能与class/class?约束组合使用，且必须跟在所有约束的后面。 部分约束有如下注意事项。\\\n使用class约束时避免使用比较操作符==/!=。这组操作符只比较引用是否相等，不进行值比较。该行为不会因具体类型是否重载比较操作符而变化，因为编译时仅能得知类型是引用类型，必须调用对所有引用类型都合法有效的实现。若要在泛型代码中进行值比较，请使用where T : IComparable\u0026lt;T\u0026gt;或where T : IEquatable\u0026lt;T\u0026gt;约束并为实际类型实现相关接口。 若类型参数没有任何约束(例如SampleClass\u0026lt;T\u0026gt;{}中的T)称为未绑定类型(unbounded type parameter)，应当遵循如下规则： 不能使用比较操作符==/!=，因为不能保证实际类型支持该操作符； 可以显式转换为任何接口类型，也可以转换成System.Object类型(或者由System.Object转换成T)； 可以与null进行比较(T是值类型时会返回false) notnull约束用以声明类型参数将约束为不可为NULL的值类型/引用类型，违反notnull约束时，编译器只会生成警告，不会报告错误。notnull约束仅在可空代码上下文中生效，若在忽略可空性的代码上下文使用，违反约束时编译器不会生成任何警告或错误。 以下对部分约束进行详细解释及代码演示。\n泛型约束常规应用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 单个类型参数的泛型约束 struct Container\u0026lt;T\u0026gt; where T : struct { public T Value { get; set; } } // 多个类型参数的泛型约束(各自约束互不干涉) class Container\u0026lt;K, V\u0026gt; where K : struct where V : class, new() { // ... } // 类型参数作为约束 class SampleClass\u0026lt;T, U, V\u0026gt; where T : V { } class List\u0026lt;T\u0026gt; { public void Add\u0026lt;U\u0026gt;(List\u0026lt;U\u0026gt; items) where U : T {/*...*/} } default约束 参考文档: dotnet/csharplang/proposals/csharp-9.0/unconstrained-type-parameter-annotations\ndefault约束是用于在重写/实现方法时消除nullable泛型重载的歧义。以下以代码演示其功能。\n关于?标注 C# 8.0中，?仅能用于显式约束了值类型或引用类型的类型参数；在C# 9.0中，?可以用于任意类型参数，无论是否存在约束。\n但要注意，?标注仅能用于设置了#nullable enable的代码上下文中。\n若是类型参数T是个引用类型，T?表示该引用类型的可NULL实例。\n1 2 var s1 = new string[0].FirstOrDefault(); // string? s1 var s2 = new string?[0].FirstOrDefault(); // string? s2 若是类型参数T是个值类型，T?表示该值类型的实例。\n1 2 var i1 = new int[0].FirstOrDefault(); // int i1 var i2 = new int?[0].FirstOrDefault(); // int? i2 若是类型参数T是其他标注过的类型U?，T?依然表示标注过的类型U?，而不是U??。\n1 2 var u1 = new U[0].FirstOrDefault(); // U? u1 var u2 = new U?[0].FirstOrDefault(); // U? u2 若是类型参数T是其他类型U，T?表示标注过的类型U?，即使在#nullable disable上下文中。\n1 2 #nullable disable var u3 = new U[0].FirstOrDefault(); // U? u3 实际上T?仅是个标注，并非一定是在构造新类型。\nFirstOrDefault方法的声明是public static TSource? FirstOrDefault\u0026lt;TSource\u0026gt;(this IEnumerable\u0026lt;TSource\u0026gt; source);，返回值部分只是声明可能为NULL，并不是将返回值变成真正的可空类型TSource?。\n对于值类型TSource(如int)，TSource?和TSource在IL中都是int，方法泛型返回值不能为int标注?(int?实际是Nullable\u0026lt;int\u0026gt;，是个新结构)。\n对于返回值，T?等同于[MaybeNull] T，对于参数值，T?等同于[AllowNull] T。当重写方法或实现接口时该等同性非常重要。\n以下代码演示了该等同性。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public abstract class A { // 以特性方式声明抽象方法 // 没有声明`class`/`struct`约束，编译器认为存在歧义，会添加`where T : default`约束 // 实际等同于public abstract T? F1\u0026lt;T\u0026gt;(); (也会默认添加where T : default约束) [return: MaybeNull] public abstract T F1\u0026lt;T\u0026gt;(); // 实际等同于public abstract void F2\u0026lt;T\u0026gt;(T? t) public abstract void F2\u0026lt;T\u0026gt;([AllowNull] T t); } public class B : A { // 重写时必须显式声明where T : default约束 public override T? F1\u0026lt;T\u0026gt;() where T : default { return defalut(T); } // matches A.F1\u0026lt;T\u0026gt;() public override void F2\u0026lt;T\u0026gt;(T? t) where T : default { } // matches A.F2\u0026lt;T\u0026gt;() } 使用T?或注解形式的可为NULL声明且不存在class/struct约束时，编译器认为存在歧义，会默认添加where T : default约束，表示没有约束class/struct。\n方法重写/实现接口时，必须声明where T : default约束，用来表示重写/实现的是不约束class/struct的方法。\\\n关于歧义 参考如下代码，除泛型约束外，两个方法是一致的形式，但方法签名不包含泛型约束信息。理论上，两个方法不应同时存在。\n1 2 3 4 5 class C { public virtual void F\u0026lt;T\u0026gt;(T? t) where T : struct { } public virtual void F\u0026lt;T\u0026gt;(T? t) where T : class { } } 实际上，以上代码是合法的，两个方法是不同的签名。\nC#8.0引入可为NULL引用类型支持，T?的意义取决于T是值类型或是引用类型。\n泛型约束 T?的含义 真实参数类型 where T : struct 可为NULL值类型 Nullable\u0026lt;T\u0026gt; where T : class 可为NULL引用类型 T(带可为NULL元数据) 以上代码在编译器视角中的实际形式如下。\n1 2 3 4 5 6 7 using System.Diagnostics.CodeAnalysis; class C { public virtual void F\u0026lt;T\u0026gt;(Nullable\u0026lt;T\u0026gt; t) where T : struct { } public virtual void F\u0026lt;T\u0026gt;([AllowNull] T t) where T : class { } } 两个方法的参数在编译器视角中是不同的类型，函数签名不同，因此可以同时存在。\n但是对常规写法而言，用户读到的代码是一样的，因此便出现了歧义。\n消除歧义 显式class/struct约束的代码，重写/实现时可以通过显式约束消除歧义。\n1 2 3 4 5 6 7 8 9 10 11 class A1 { public virtual void F1\u0026lt;T\u0026gt;(T? t) where T : struct { } public virtual void F1\u0026lt;T\u0026gt;(T? t) where T : class { } } class B1 : A1 { public override void F1\u0026lt;T\u0026gt;(T? t) /*where T : struct*/ { } public override void F1\u0026lt;T\u0026gt;(T? t) where T : class { } } 注: 显式struct约束的方法参数实际是Nullable\u0026lt;T\u0026gt;，相对于参数为T的方法，这是个约束更强(更具体)的版本，在编译器解析时具备更高的优先级。\n因此重写方法时的where T : struct约束可以省去，但非常不建议这样做。\n而当存在有约束和无约束版本的方法时，可以使用default约束来消除歧义。\n1 2 3 4 5 6 7 8 9 10 11 class A2 { public virtual void F2\u0026lt;T\u0026gt;(T? t) where T : struct { } public virtual void F2\u0026lt;T\u0026gt;(T? t) { } } class B2 : A2 { public override void F2\u0026lt;T\u0026gt;(T? t) /*where T : struct*/ { } public override void F2\u0026lt;T\u0026gt;(T? t) where T : default { } } where T : default声明此处重写的是无class/struct约束的版本。\nunmanaged约束 该约束可用于期望以直接操作内存块的方式读写类型实例的代码，如下所示。\n1 2 3 4 5 6 7 8 9 unsafe public static byte[] ToByteArray\u0026lt;T\u0026gt;(this T argument) where T : unmanaged { var size = sizeof(T); var result = new Byte[size]; Byte* p = (byte*)\u0026amp;argument; for (var i = 0; i \u0026lt; size; i++) result[i] = *p++; return result; } 由于对未知的类型T调用sizeof操作符，因此代码必须声明为不安全上下文(unsafe context)； 若没有将类型参数T约束为unmanaged，sizeof操作符不可用。\n委托约束 C#语言规范中未提供类似where T : delegate形式的约束，但可以用类型约束来实现类似的功能。 可以将T约束为System.Delegate/System.MulticastDelegate的子类，就可以在满足类型安全的前提下对委托进行操作。 代码演示如下所示。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 操作委托的扩展方法 // 合并相同相同类型的委托 public static T? TypeSafeCombine\u0026lt;T\u0026gt;(this T source, T target) where T : Delegate =\u0026gt; Delegate.Combine(source, target) as T; // 定义两个相同类型的委托 Action first = () =\u0026gt; Console.WriteLine(\u0026#34;this\u0026#34;); Action second = () =\u0026gt; Console.WriteLine(\u0026#34;that\u0026#34;); // 合并\u0026amp;调用 var combined = first.TypeSafeCombine(second); // √ combined!(); // 定义一个不同类型的委托 Func\u0026lt;bool\u0026gt; test = () =\u0026gt; true; // 如下的调用是错误的 var badCombined = first.TypeSafeCombine(test); // × 枚举约束 与委托调用类似，C#语言规范中并未直接提供纯粹的where T : enum约束(unmanaged约束允许传入枚举，但不是只允许枚举)，但可以用类型约束来实现类似功能。 可以将T约束为System.Enum的子类，就可以约束为仅允许枚举传入。 代码示例如下所示。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 操作枚举的扩展方法 // 将给定枚举的值和名称构造成字典对象 public static Dictionary\u0026lt;int, string\u0026gt; EnumNamedValues\u0026lt;T\u0026gt;() where T : Enum { var result = new Dictionary\u0026lt;int, string\u0026gt;(); var values = Enum.GetValues(typeof(T)); foreach (int item in values) { result.Add(item, Enum.GetName(typeof(T), item)!); } return result; } // 定义枚举 enum SomeValue {A, B, C, D} // 调用扩展方法并初始化字典 var dict = EnumNamedValues\u0026lt;SomeValue\u0026gt;(); 类型参数继承/实现声明的类/接口(F-Bound) 本质是F-Bound，此处仅简要介绍官方文档对该约束的介绍及演示(样例中涉及到对C#支持接口静态成员的语言功能)，请参考下文(F-Bound)对该技巧的详细介绍。\n1 2 3 4 5 6 7 // T必须实现接口本身 interface IAdditionSubtraction\u0026lt;T\u0026gt; where T : IAdditionSubtraction\u0026lt;T\u0026gt; { // C#接口支持声明静态成员 static abstract T operator +(T left, T right); static abstract T operator -(T left, T right); } allows ref struct约束 该约束是C#13.0新增的语言功能。\nallows ref struct实际上是反约束，允许指定的类型参数可以是ref struct类型，因此该类型实例必须遵守如下规则：\n不能被装箱 必须遵守引用安全规则(ref safety rules) 不能在禁止使用ref struct的地方使用该类型，例如static字段 实例可以使用scoped修饰符进行标记 F-Bound F-Bound是一种泛型编程技巧，通常用于面向对象编程中，帮助在继承层次结构中实现类型自参的自引用。简单来说，是指通过继承某个类型的泛型类，子类可以返回类型自己。这个技巧在做流式接口设计、构建DSL或者处理复杂类型时非常有用。\nF-Bound通常用于实现以下模式：基类定义了一个泛型方法，该方法返回该类（或者子类）的类型。而子类继承基类时，能够实现这个方法并返回自己的类型。\n以下代码演示基本应用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 using System; // 基类 Shape，使用 F-Bound 技巧 public abstract class Shape\u0026lt;T\u0026gt; where T : Shape\u0026lt;T\u0026gt; { // 一个设置颜色的方法，返回当前类型（T）本身 public T SetColor(string color) { Console.WriteLine($\u0026#34;Setting color to {color}\u0026#34;); return (T)this; // 返回当前类型本身 } // 计算形状的面积（可以在子类中实现具体的面积计算） public abstract double GetArea(); } // Circle 类，继承自 Shape\u0026lt;T\u0026gt;，并指定自己为 T public class Circle : Shape\u0026lt;Circle\u0026gt; { public double Radius { get; set; } public Circle(double radius) { Radius = radius; } public override double GetArea() { return Math.PI * Radius * Radius; } // 可以链式调用 SetColor 方法 public Circle SetRadius(double radius) { Radius = radius; return this; } } // Rectangle 类，继承自 Shape\u0026lt;T\u0026gt;，并指定自己为 T public class Rectangle : Shape\u0026lt;Rectangle\u0026gt; { public double Width { get; set; } public double Height { get; set; } public Rectangle(double width, double height) { Width = width; Height = height; } public override double GetArea() { return Width * Height; } // 可以链式调用 SetColor 方法 public Rectangle SetDimensions(double width, double height) { Width = width; Height = height; return this; } } // 测试 F-Bound class Program { static void Main(string[] args) { // 创建一个 Circle 对象，设置颜色和半径 Circle circle = new Circle(5); circle.SetColor(\u0026#34;Red\u0026#34;) .SetRadius(10); Console.WriteLine($\u0026#34;Circle Area: {circle.GetArea()}\u0026#34;); // 创建一个 Rectangle 对象，设置颜色和尺寸 Rectangle rectangle = new Rectangle(4, 5); rectangle.SetColor(\u0026#34;Blue\u0026#34;) .SetDimensions(6, 7); Console.WriteLine($\u0026#34;Rectangle Area: {rectangle.GetArea()}\u0026#34;); } } 可变性 可变性(variance)包含协变(covariance)、逆变(contravariance)和不变(invariance)。\n语言层面的可变性支持是类型系统支持多态的关键部分，有的是由严格的语法支持，有的语言通过子类型兼容性简介体现。\n简单来讲，协变(covariance)是指子类型可以赋值给父类型，常用于返回值(输出)场景； 逆变(contravariance)是指父类型可以赋值给子类型，常用于参数(输入)场景； 不变(invariance)是指完全相同的类型才能赋值，最安全也最严格。\n可变性的核心概念(类型转换中派生类与基类之间的方向)在非泛型场景中也存在，但一般是隐式的，或称为“类型兼容性”问题。 有些“类型兼容性”问题(例如数组协变)依赖编译器推断，但在运行时检查，可能抛出异常。 常规场景中，参数逆变，返回值协变，便已是足够完善可靠的规则。\n语言 协变(covariant) 逆变(contravariant) 说明 C# ✅ out ✅ in 泛型接口/委托支持，数组协变（但运行时检查） Java ✅ ? extends ✅ ? super 使用通配符在泛型中表达变体，没有关键字（只能在使用处） Scala ✅ +T ✅ -T 在泛型定义中支持变体标注，极其灵活 Python ✅（Typing 协变） ✅（Typing 逆变） 使用 typing.Generic + covariant=True 表达 Kotlin ✅ out T ✅ in T 类似 C#，在泛型类型定义中支持关键字 Swift ✅ 协变支持 ✅（使用 protocol） 支持返回值协变、协议泛型逆变较间接 TypeScript ✅ 自动推导 ✅ 自动推导 没有变体关键字，但函数/接口参数/返回值类型由结构类型系统自动推导 Rust ⚠️ 不支持协变/逆变关键字 ⚠️ 不支持显式变体 有复杂的生命周期变体（\u0026lsquo;a: \u0026lsquo;b）机制，不针对泛型类型参数 语言 泛型定义处变体 使用处变体 自动推导 安全检查 使用推荐度 C# ✅ in/out ✅ 委托 ❌ ✅ 编译时 ✅✅✅ Java ❌ ✅ ? extends / ? super ❌ ✅ 编译时 ✅✅ Scala ✅ +T / -T ✅ ✅ ✅ ✅✅✅✅ Kotlin ✅ out T / in T ✅ ✅ ✅ ✅✅✅ Swift ❌ 部分支持 ❌ ✅ ✅✅ TypeScript ❌ ✅ ✅✅✅ ✅（结构化） ✅✅✅ Python ✅ (Typing) ✅ ❌ ✅（类型检查器） ✅✅ Rust ❌ 泛型不支持 ❌ ❌ ✅（生命周期） ✅（不同维度） 上表整理自ChatGPT,其中Kotlin/Swift/TypeScript/Rust的相关内容尚未审慎求证。\nC# 官方文档:\\\nCovariance and contravariance in generics Covariance and Contravariance (C#) - Overview Variance in Generic Interfaces (C#) Creating Variant Generic Interfaces (C#) Using Variance in Interfaces for Generic Collections (C#) Variance in Delegates (C#) Using Variance in Delegates (C#) Using Variance for Func and Action Generic Delegates (C#) 泛型接口/委托中的可变性 泛型类、泛型方法中不能独立使用可变性修饰符in/out\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // 定义泛型接口(实际是标准库中的类型，此处用以演示) // 比较器接口(逆变) public interface IEqualityComparer\u0026lt;in T\u0026gt; { bool Equals(T? x, T? y); int GetHashCode([DisallowNull] T obj); } // 只读集合接口(协变) public interface IReadOnlyList\u0026lt;out T\u0026gt; : IEnumerable\u0026lt;T\u0026gt;, IEnumerable, IReadOnlyCollection\u0026lt;T\u0026gt; { T this[int index] { get; } } // 简单的继承关系 class Base { } class Derived : Base { } // 实现基类的比较器 class BaseComparer : IEqualityComparer\u0026lt;Base\u0026gt; { public int GetHashCode(Base baseInstance) { return baseInstance.GetHashCode(); } public bool Equals(Base? x, Base? y) { return x == y; } } // 基类的比较器可以作为派生类的比较器使用，此处体现了逆变 IEqualityComparer\u0026lt;BaseClass\u0026gt; baseComparer = new BaseComparer(); IEqualityComparer\u0026lt;DerivedClass\u0026gt; childComparer = baseComparer; // 子类的集合可以作为基类的集合的使用，体现了协变 IReadOnlyList\u0026lt;Derived\u0026gt; childList = []; IReadOnlyList\u0026lt;Base\u0026gt; baseList = childList; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 泛型委托定义，参数逆变，返回值协变 public delegate TResult Func\u0026lt;in T, out TResult\u0026gt;(T arg); // 两个委托实例 // 委托1：接受派生类，返回基类 Func\u0026lt;Derived, Base\u0026gt; func1 = (Derived d) =\u0026gt; { Console.WriteLine(d); return new Base(); }; // 委托2：接受基类，返回派生类 Func\u0026lt;Base, Derived\u0026gt; func2 = (Base b) =\u0026gt; { Console.WriteLine(b); return new Derived(); }; // 参数逆变：委托1期望接受派生类，实际可以接受基类 // 返回值协变：委托1期望返回基类，实际可以返回派生类 // 因此以下赋值是有效的(类型兼容)，委托2的实例可以作为委托1的实例调用 func1 += func2; 非泛型场景中的可变性 数组协变(强烈不推荐使用) 1 2 3 4 5 6 // 构造字符串数组实例，可以赋值给对象数组 object[] array = new string[] { \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34; }; // 一般的读取不会有类型安全问题 var value = array[0]; // 赋值语句可以通过编译，但会在运行时抛出异常 array[0] = 1; 类型不安全。\n该功能是编译器隐式支持的功能，但在运行时检查安全性。\n协变的数组变量不存在严格的写保护，极有可能出现不经意的赋值操作，生产环境中应当避免使用。\n方法重写返回值类 1 2 3 4 5 6 7 8 9 10 11 class A { // 基类型中定义虚方法，返回值是基类型A public virtual A CreateInstance() { return new A(); } } class B : A { // 派生类中重写方法，返回值类型是派生类型B，返回值协变 public override B CreateInstance() { return new B(); } } 类型安全。\n该功能是编译器隐式支持的功能。\n虚方法在子类中重写时，参数必须保持一致，但返回值可以是更具体的类型(返回值是协变的)，两个方法依然是“类型兼容”的。\n非泛型委托的协变和逆变 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 定义基类型Animal及派生类型Dog class Animal { } class Dog : Animal { } // 定义委托类型 delegate Animal Creator(); delegate void Handler(Dog dog); // 定义普通方法 static Dog CreateDog() { return new Dog(); } static void FeedAnimal(Animal animal) { } // 方法可以构造为委托实例 Creator creator = CreateDog; // 返回值协变 Handler handler = FeedAnimal; // 参数逆变 类型安全。\n该功能是编译器隐式支持的功能。\n委托Creator期望生产Animal实例，实际上得到Dog实例，Dog作为Animal使用是类型安全的，因此Dog生产者作为Animal生产者也是类型安全的。这体现的是协变。\n委托Handler期望消费Dog实例，因此调用Handler时给出的必定是Dog(或Dog派生类)实例，现在handler实际上是个消费Animal的实例，Dog作为Animal使用是类型安全的，消费Animal的方法消费Dog也是类型安全的，因此Animal消费者作为Dog消费者也是类型安全的。这体现的是逆变。\n委托可以视为类型安全的函数类型，函数类型也可以视为构造类型(由参数类型+返回类型构造),在这个视角看待类型兼容也许有不一样的发现。可参考本站文章CTFP(category theory for programmer/面向程序员的范畴论)中协变和逆变的相关内容。\n高阶泛型 Higher Kind Higher Rank 参考链接 Generic programming\nParametric polymorphism\n本文内容经过较长时间整理、\n","date":"2025-05-16T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/language/generic/","title":"泛型"},{"content":"Markdown语法记录 常规内容语法此处不再详细介绍，仅记录偶尔需要查阅语法的内容类型。\n参考链接 CommonMark Spec GitHub Flavored Markdown Spec GitLab Flavored Markdown (GLFM) 硬换行 文档: CommonMark Spec - 6.7 Hard line breaks\n连续的多行内容若不加特殊处理总是会渲染成没有换行的整个段落，手动换行可以通过插入空行、行尾空格(两个及以上)、反斜杠(\\)来实现。\n删除线 文档: GitHub Flavored Markdown Spec - 6.5 Strikethrough (extension)\n该功能由strikethrough扩展支持，但一般默认启用。\n一对~~包裹的连续内容会用删除线格式化。\n~~删除线~~ -\u0026gt; 删除线\n任务列表 文档: GitHub Flavored Markdown Spec - 5.3 Task list items (extension)\n该功能由tasklist扩展支持，但一般默认启用。\n1 2 + [ ] 任务一 + [x] 任务二 任务一 任务二 diff code block 该语法是用于展示代码增删情况，开启该功能一般不需要特殊配置。\n代码块语言类型需声明为diff，在删除的行前加-，在新增的行前加+。\n1 2 3 4 5 6 7 ```diff [dependencies.bevy] git = \u0026#34;https://github.com/bevyengine/bevy\u0026#34; rev = \u0026#34;11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13\u0026#34; - features = [\u0026#34;dynamic\u0026#34;] + features = [\u0026#34;jpeg\u0026#34;, \u0026#34;dynamic\u0026#34;] ``` 1 2 3 4 5 [dependencies.bevy] git = \u0026#34;https://github.com/bevyengine/bevy\u0026#34; rev = \u0026#34;11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13\u0026#34; - features = [\u0026#34;dynamic\u0026#34;] + features = [\u0026#34;jpeg\u0026#34;, \u0026#34;dynamic\u0026#34;] GitHub Alerts 文档: Basic writing and formatting syntax - Alerts\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026gt; [!NOTE] \u0026gt; Useful information that users should know, even when skimming content. \u0026gt; [!TIP] \u0026gt; Helpful advice for doing things better or more easily. \u0026gt; [!IMPORTANT] \u0026gt; Key information users need to know to achieve their goal. \u0026gt; [!WARNING] \u0026gt; Urgent info that needs immediate user attention to avoid problems. \u0026gt; [!CAUTION] \u0026gt; Advises about risks or negative outcomes of certain actions. GitLab Alerts 文档: GitLab Flavored Markdown (GLFM) - Alerts\nGitLab \u0026gt;= 17.10\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026gt; [!note] \u0026gt; The following information is useful. \u0026gt; [!tip] \u0026gt; Tip of the day. \u0026gt; [!important] \u0026gt; This is something important you should know. \u0026gt; [!warning] \u0026gt; The following would be dangerous. \u0026gt; [!caution] \u0026gt; You need to be very careful about the following. 此外还支持设定标题。\n1 2 \u0026gt; [!warning] Data deletion \u0026gt; The following instructions will make your data unrecoverable. 也支持GitLab段落语法\n1 2 3 4 5 6 \u0026gt;\u0026gt;\u0026gt; [!note] Things to consider You should consider the following ramifications: 1. consideration 1 1. consideration 2 \u0026gt;\u0026gt;\u0026gt; 图表 Mermaid提供支持，文档: Diagram Syntax\n语法随版本升级变动频繁，需要确认平台集成的版本情况。\n1 2 3 ```mermaid info ``` ","date":"2025-05-12T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/snippet/markdown-snippets/","title":"Markdown语法记录"},{"content":"Java语言功能更新 当前整理范围: JDK9-\u0026gt;JDK21(LTS)\n参考链接 A categorized list of all Java and JVM features since JDK 8 to 21 by Dávid Csákvári New language features since Java 8 to 21 by Dávid Csákvári Java 9 到 21 的语言特性更新 by Alex Tan - 本文是引文2中文翻译\u0026amp;整理 Java Language and Virtual Machine Specifications Java Platform, Standard Edition Java Language Updates, Release 21 - Oracle官方整理的Java SE 9到特定版本的语言功能更新(修改URL中的版本号可以查看9到其他版本的功能更新) JEP 0: JEP Index 参考以上内容进消化整理\nJava 9 该版本新增的语言功能详情可参考: JEP 213: Milling Project Coin\n1. 接口中允许私有方法 Java8允许接口实现默认方法，在Java9中允许接口实现私有方法。可以将期望复用但又不想公开暴露的代码放到私有方法中，由默认方法调用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 public interface Interface { default void op1() { // code ... common(); } default void op2() { // code ... common(); } private void common() { // common code } } 2. 匿名内部类的钻石操作符 Java7引入钻石操作符\u0026lt;\u0026gt;，调用构造器时允许编译器推断泛型类型，降低没必要的类型参数声明，例如：List\u0026lt;Integer\u0026gt; numbers = new ArrayList\u0026lt;\u0026gt;();。但该功能并不支持匿名内部类，相关邮件: Diamond operator and anonymous classes。\n相关问题已在Java9解决，此后以下代码可被接受了。\n1 2 3 4 5 6 7 8 9 10 11 12 Callable\u0026lt;Integer\u0026gt; task = new Callable\u0026lt;\u0026gt;() { public Integer call() { return doSomething(); } }; Comparator\u0026lt;MyClass\u0026gt; FORWARD = new Comparator\u0026lt;\u0026gt;() { @Override public int compare(MyClass first, MyClass second) { // comparator logic } }; 3. effectively-final变量在try-with-resources语句中可作为资源使用 Java7引入try-with-resources语句来简化资源释放，示例如下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // java7之前需要手动管理资源释放 BufferedReader br = new BufferedReader(...); try { return br.readLine(); } finally { if (br != null) { br.close(); } } // java7可以用该语句自动释放资源，减少样板代码 try (BufferedReader br = new BufferedReader(...)) { return br.readLine(); } 实际使用中，也会存在一些短板，示例如下。\n1 2 3 4 5 6 7 8 9 10 // 多个资源的时候代码有点难读 try (BufferedReader br1 = new BufferedReader(...); BufferedReader br2 = new BufferedReader(...)) { System.out.println(br1.readLine() + br2.readLine()); } // 处理已有资源的时候仍需要定义变量 try (Closeable c = resouce) { // ... } Java9解决了这些问题，现在也可以处理不可变或实际不可变的局部变量，示例如下。\n1 2 3 4 5 BufferedReader br1 = new BufferedReader(...); BufferedReader br2 = new BufferedReader(...); try (br1; br2) { System.out.println(br1.readLine() + br2.readLine()); } 注意，由于变量在try-with-resources语句外可用，但此时资源已被释放，所以操作基本都会失败，要特别注意。示例如下。\n1 2 3 4 5 BufferedReader br = new BufferedReader(...); try (br) { System.out.println(br.readLine()); } br.readLine(); // Boom! 4. 下划线不可作为标识符使用 Java8中将_作为标识符使用时，编译器会输出警告。Java9更进一步，将此情形视为错误，这是保留该符号未来做特殊使用。\n1 2 3 4 5 6 // java7-: 变量可用 // java8: 编译警告 变量可用 // java9 -\u0026gt; 20: 编译错误 // java21(开启预览功能): 编译通过 变量不可用 // java22+: 编译通过 变量不可用 int _ = 10; 5. 警告改进 @SafeVarargs注解只能用于不可被覆写的方法，此前只包括构造方法、静态方法、final实例方法。 实际上私有实例方法也算是，现在可以将@SafeVarargs注解在私有方法上用以抑制Type safety: Potential heap pollution via varargs parameter警告。 可变参数和泛型两者结合使用时可能产生问题，编译器会输出警告，当程序员确保没有进行危险操作时，使用该注解抑制警告。 注解使用示例如下(参考自Annotation Type SafeVarargs)。\n1 2 3 4 5 6 7 8 9 // 此前只能用于标记构造函数、static方法、final方法 @SafeVarargs // 实际上并不安全 static void m(List\u0026lt;String\u0026gt;... stringLists) { Object[] array = stringLists; List\u0026lt;Integer\u0026gt; tmpList = Arrays.asList(42); array[0] = tmpList; // 该赋值实际上不合法、但编译会正常通过 String s = stringLists[0].get(0); // 运行时抛出ClassCastException } 此外编译器不再为引入废弃类型告警，这些警告已经在调用的地方展示了。参考JEP 211: Elide Deprecation Warnings on Import Statements\nJava 11 (LTS) 局部变量类型推断 Java 10(JEP 286: Local-Variable Type Inference)中引入(Lambda中尚不支持)，Java 11(JEP 323: Local-Variable Syntax for Lambda Parameters)中得到改进\n该功能允许声明局部变量时可以略去显式类型声明，变量类型由编译器推断，某些场景中可以使代码精炼易读。如下所示。\n1 var awesome = new MyAwesomeClass(); 注意：与动态类型无关。\n但也有些场景，使用显式类型声明更具优势，以下通过几个例子讨论。\n在源码中移除显式的类型信息，可能会影响可读性。IDE能起到一定帮助，但是在代码评审或快速浏览代码时就会有影响。例如在工厂模式或构造者模式中，就要找到负责构建对象的代码，才能推断出其类型。参考以下示例。\n1 2 3 var date = LocalDate.parse(\u0026#34;2019-08-13\u0026#34;); // LocalDate var dayOfWeek = date.getDayOfWeek(); // java.time.DayOfWeek var dayOfMonth = date.getDayOfMonth(); // int 对以上示例，若使用Joda库，调用date.getDayOfWeek()时返回值为整型，同样的方法名在不同的库中返回值也不一定相同。 当代码较长时，翻看上下文确定类型变得更加困难，因此使用var要时刻考虑代码可读性。\n时刻考虑可读性\n此外，var消除所有可用的类型信息，甚至导致无法被推断。绝大多数情形中编译器能捕获问题，例如var不能针对lambda和方法引用进行推断，因为编译器依赖于表达式左边的声明来确定类型。但也存在一些例外。\n比如当和钻石操作符搭配使用时。例如var map = new HashMap\u0026lt;\u0026gt;();，语法上是可行的，编译器甚至不会报出警告。然而完全不指定泛型类型，类型推断会给出Map\u0026lt;Object, Object\u0026gt;的结果，这大概率并非预期。此时可以考虑常规语法Map\u0026lt;String, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;();，或者替换掉类型操作符var map = new HashMap\u0026lt;String, String\u0026gt;();。\n与基础类型使用时也会有问题，例如byte b = 1/short s = 1/int i = 1/long l = 1/floate f = 1/double d = 1，没有显式类型声明时，类型会被推断为int。处理基本类型时，要么使用类型字面量(例如1L/1.0F/1.0D)，要么完全不使用var。\n保留重要的类型信息\n参考官方针对var的代码风格指南(Local Variable Type Inference Style Guidelines)和常见问题(Local Variable Type Inference Frequently Asked Questions)。\n使用中尽量以保守的态度引入代码，在局部变量中使用，使之尽量在较小的作用域中生效。\n仍需强调，var并非是个新的关键字，而是个保留类名。当它用作类型名的时候是有特殊含义的，其他场景下依然是个合法的标识符。例如var var = 10是个合法的声明。\n目前尚不存在特定的关键字来声明不可变(例如val或const)，目前可以用final var达成预期。\n参考官方的代码风格指南\nJava 14 switch表达式 可用: 14-JEP 361: Switch Expressions (预览: 12-JEP 325: Switch Expressions (Preview); 13-JEP 354: Switch Expressions (Second Preview))\nswitch不再只是语句，可以是表达式，但要注意两者语法略有区别。\n语句示例如下，官方文档请参考The switch Statement\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // switch语句语法，逻辑上是if-else-if的简化。 // 接受的数据类型如下: // 1. 基本数据类型: byte short int char // 2. 包装数据类型: Byte Short Integer Character // 3. 字符串(Java SE 7开始支持) // 4. 枚举类型(Java SE 7开始支持) // 注意： // 1. 缺少break会导致的击穿(fall-through，即代码连续执行)； // 2. null会导致抛出空指针异常 // 3. 所有case共享一个作用域(临时变量不允许重复定义) switch (expr) { case value1: // code break; case value2: case value3: // code break; default: // code } 表达式示例如下，可用于任何接受表达式的地方，例如函数参数。\n1 2 3 4 5 6 7 8 9 int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -\u0026gt; 6; case TUESDAY -\u0026gt; 7; default -\u0026gt; { String s = day.toString(); int result = s.length(); yield result; } }; 表达式不存在击穿，可以为case指定多个常量，逗号分隔。 分支可以是单个表达式，也可是语句块中的多条语句(必须以yield value;语句返回该语句块的求值结果)。 分支是独立作用域，因此可以随意定义同名变量。 表达式必须覆盖所有条件，对于常规的字符串、基本类型及其包装类型等，必须提供default分支；对于枚举，要么显式声明所有情况，要么提供default，前者是最佳实践，当枚举值变更时，编译阶段即可发现错误。 推荐使用表达式而不是语句\n此处同时记录类似语句的冒号语法，但强烈不建议使用！\n1 2 3 4 5 6 7 int result = switch (s) { case \u0026#34;foo\u0026#34;: case \u0026#34;bar\u0026#34;: yield 2; default: yield 3; }; 这种形式也能作为表达式使用，但击穿和共享作用域也出现了。强烈不推荐，使用箭头形式的表达式即可，避免所有问题。\nJava 15 1. 文本块 可用: 15-JEP 378: Text Blocks (预览: 13-JEP 355: Text Blocks (Preview); 14-JEP 368: Text Blocks (Second Preview))\n1 2 3 4 5 6 7 8 9 String html = \u0026#34;\u0026#34;\u0026#34; \u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;Hello, world\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; \u0026#34;\u0026#34;\u0026#34;; System.out.println(html); 该功能简化多行文本初始化工作。文本块与字符串字面量相似，但换行和引号都不需要转义。\n文本块以\u0026quot;\u0026quot;\u0026quot;开始，后面紧跟换行；以\u0026quot;\u0026quot;\u0026quot;结束，可以紧跟最后一行，也可以单独一行。源代码中每个换行的地方都会产生一个\\n字符。\n文本段可以和相邻的Java代码对齐，编译器会检查每行用于缩进的空格，找到缩进最少的行，然后将每行都转化为这个相同的最少缩进。若结尾的\u0026quot;\u0026quot;\u0026quot;是在一个单独的行，注意它的缩进情况，有可能会整体的缩进发生改变。开头的\u0026quot;\u0026quot;\u0026quot;不会影响缩进移除，所以文本块没有必要和其对齐。\nString类提供了编程方式处理缩进的方法，indent方法接受一个整数参数，返回新的符合特定缩进级别的字符串；stripIndent方法返回移除所有缩进的字符串。\n此外，使用文本块需要注意以下事项：\n尚不支持插值，当前可以用String::formatted及String::format来实现。 行尾空格通常会被忽略掉，当需要行尾空格时，要在行尾添加\\s或\\t。 换行使用\\n，与当前操作系统或源代码采用的换行符无关，若存在兼容性问题，可以用String::replace进行替换。 源代码可以采用任意类型的缩进：制表符、空格或者两者混用，但文本块的每行必须使用一致的缩进。 2. 携带详细信息的空指针异常 可用: 15-Enable ShowCodeDetailsInExceptionMessages by default (14-JEP 358: Helpful NullPointerExceptions 启动程序时需指定-XX:+ShowCodeDetailsInExceptionMessages参数)\n1 2 3 4 5 6 7 8 9 10 node.getElementsByTagName(\u0026#34;name\u0026#34;).item(0).getChildNodes().item(0).getNodeValue(); // 以前的异常信息 Exception in thread \u0026#34;main\u0026#34; java.lang.NullPointerException at Unlucky.method(Unlucky.java:83) // 现在的异常信息，包含不能被执行的步骤，及其失败原因 Exception in thread \u0026#34;main\u0026#34; java.lang.NullPointerException: Cannot invoke \u0026#34;org.w3c.dom.Node.getChildNodes()\u0026#34; because the return value of \u0026#34;org.w3c.dom.NodeList.item(int)\u0026#34; is null at Unlucky.method(Unlucky.java:83) 空指针异常扩展是在JVM层面实现的，即使代码编译为较老的Java版本或者其他的JVM语言(例如Scala或Kotlin)也同样能受益。\n但并非所有的空指针异常都能得到这些额外的信息，仅仅是被JVM创建并抛出的才行：\n对null读写字段 对null调用方法 对null数据读写元素(索引信息不会输出) 对null拆箱 此外，该功能不支持序列化。例如通过RMI方式远程调用代码发生异常时，异常信息里不会包含相关信息。\nJava 16 1. 记录类 可用: 16-JEP 395: Records (预览: 14-JEP 359: Records (Preview); 15-JEP 397: Sealed Classes (Second Preview))\n其它相关内容: Data Classes and Sealed Types for Java; Records Come to Java\n记录类给Java带来了数据类和封闭类型(Data Classes and Sealed Types for Java)，用于携带不可变数据。记录类可以视为元组(tuple)。\n1 public record Point(int x, int y) {} 以上简练的语法声明了一个记录类Point，其具备以下定义\n两个private final字段int x和int y 一个接受x和y作为参数的构造器 字段getter方法x()和y() 纳入字段x和y的方法hashCode、equals和toString 使用方式与普通类一致。\n1 2 3 var point = new Point(1, 2); point.x(); // returns 1 point.y(); // returns 2 记录类设计为浅层不可变数据(shallowly immutable data)的透明载体(transparent carriers)，定义和使用都存在限制。\n记录类的字段默认final，实际上不允许非final字段。 定义记录时必须提供所有字段，记录体内不允许定义额外字段。 允许定义额外的构造方法来提供默认值，但无法隐藏包含所有字段的标准构造方法(canonical constructor)。 不能继承其它类，是隐式final的，不能被继承。 不能是抽象的。 不能声明native方法。 记录类强调不可变性，字段赋值只能通过构造方法。 编译器自动添加的隐式标准构造方法和类具有一致的可见性，显式声明标准构造方法时其可见性不低于类本身。 可以自定义构造方法，可以是任意可见性，但其实现必须最终委派给标准构造方法完成初始化。\n对于其中的每个字段，会自动生成其getter，名称与字段名一致。可以显式定义甚至重写。\n记录类可以拥有静态方法和实例方法。\n代码示例如下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 record Point(int x, int y) { // 显式声明标准构造方法，对数据进行验证，只能保持或放大可见性 public Point { if (x \u0026lt; 0) { throw new IllegalArgumentException(\u0026#34;x \u0026gt;= 0\u0026#34;); } if (y \u0026lt; 0) { // 只能在标准构造方法中进行赋值操作 y = 0; } } // 声明额外构造方法，委派给标准构造方法，可以限制可见性 Point(int i) { this(i, i); } // 声明额外构造方法，委派给其它构造方法，最终由标准构造方法完成初始化，可以限制可见性 private Point() { this(0); } // 显式定义或重写默认为x提供的getter，同样可以处理hashCode equals toString @Override public int x() { // code... return x; } // 支持静态方法 static Point zero() { // 调用了私有构造方法 return new Point(); } // 支持实例方法 boolean isZero() { return x == 0 \u0026amp;\u0026amp; y == 0; } } 记录类专注于承载数据，因此定制性略有受限。但也是得益于这样的设计，序列化也十分容易且安全(相比于常规的类)。\n记录类的实例能被序列化和反序列化。然而不能通过提供writeObject readObject readObjectNoData writeExternal或 readExternal方法来自定义其处理流程。记录类的组件（字段）负责序列化，而记录类的标准构造方法掌管反序列化。\n序列化正好基于成员变量的状态，反序列化又总会调用标准构造方法，所以不可能创建一个无效状态的记录类。 从开发者角度看，序列化和反序列化与以往一样。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public record Point(int x, int y) implements Serializable { } public static void recordSerializationExample() throws Exception { Point point = new Point(1, 2); // 序列化 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(\u0026#34;tmp\u0026#34;)); oos.writeObject(point); // 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(\u0026#34;tmp\u0026#34;)); Point deserialized = (Point) ois.readObject(); } 关于记录类，推荐注意以下实践。\n使用本地记录类来构建中间转化变量 复杂的数据转换需要中间变量，Java 16之前的典型方案是依赖于Pair或三方库里相似的holder类，再或者是自己定义（可能是静态内部）类来承载数据。前者通常不够灵活，后者又在仅用于单个方法的上下文中引入了其它类，污染了命名空间。虽然也可以在方法体中定义类，但也因为其啰嗦的语法很少这么用。\nJava 16进行了改进，可以在方法体中定义本地记录类。\n1 2 3 4 5 6 7 8 9 10 public List\u0026lt;Product\u0026gt; findProductsWithMostSaving(List\u0026lt;Product\u0026gt; products) { record ProductWithSaving(Product product, double savingInEur) {} products.stream() .map(p -\u0026gt; new ProductWithSaving(p, p.basePriceInEur * p.discountPercentage)) .sorted((p1, p2) -\u0026gt; Double.compare(p2.savingInEur, p1.savingInEur)) .map(ProductWithSaving::product) .limit(5) .collect(Collectors.toList()); } 记录类紧凑的语法正好契合 Steam API 紧凑的语法。\n除了记录类外，这个改进也适用于本地枚举甚至接口。\n检查引用的类库 记录类没有遵循JavaBeans的约定，一些遵循约定的工具类和记录类可能不能正常使用。\n没有默认的构造方法 没有setter方法 访问器方法不依照getX()格式 例如，记录类不能用作JPA(例如Hibernate)的实体。有一些关于JPA遵循记录类规范的讨论，但迄今为止没找到相关开发进度的报道。但也有文章指出将记录类能应用到项目中且没有问题。\n大多数Dávid Csákvári试过的工具类（包括Jackson，Apache Commons Lang，JSON-P，Guava ）都支持记录类，但还存在些小问题。比如，流行的JSON库Jackson较早就支持记录类。它的大多数特性，包括对记录类和 JavaBeans 的序列化、反序列化都没什么问题，但操控对象的特性还没适配。\nSpring也在许多情况下对记录类提供开箱即用的支持，包括序列化甚至依赖注入，但许多 Spring 应用程序使用的ModelMapper库不支持将JavaBeans映射到记录类。\n建议在使用记录类前先升级并检查使用的工具库，避免意外之喜，但大体上来说，可以认为流行的工具库已经涵盖了大部分特性。\n参看关于记录类的工具库集成试验。\n使用模式匹配快速访问字段 实践中建议考虑使用switch模式匹配及instanceof模式匹配与记录类结合使用，便捷地解构对象。\n2. instanceof模式匹配 可用: 16-JEP 394: Pattern Matching for instanceof (预览: 14-JEP 305: Pattern Matching for instanceof (Preview); 15-JEP 375: Pattern Matching for instanceof (Second Preview))\n1 2 3 4 5 6 7 8 9 10 // 传统形式代码，需要进行显式类型转换 if (obj instanceof String) { String s = (String) obj; // code } // 现在更简练 if (obj instanceof String s) { // code } 该模式是类型检查(obj instanceof String)和模式变量(s)的组合，类型检查和旧的instanceof操作符几乎一样，但模式确定无法匹配的话会导致编译错误。示例如下。\n1 2 3 4 5 // 该模式匹配必定失败，因此在编译时报错 Integer i = 1; if (i instanceof String s) { // code } 仅在检查通过时，模式变量才会从目标变量中提取出来。几乎和常规的非final变量一样，值能被修改、会隐藏(shadow)字段、同名变量引发编译错误。但模式变量作用域是基于控制流分析确定的，仅限在**明确匹配(definitely matched)**的作用域内生效，甚至支持更复杂的情形。\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 可以用于后续判断 if (obj instanceof String s \u0026amp;\u0026amp; s.length() \u0026gt; 5) { // code... } // 也支持提前返回值或抛出异常 private static int getLength(Object obj) { if (!(obj instanceof String s)) { throw new IllegalArgumentException(); } // 代码是正确的，此处是模式变量的作用域 return s.length(); } 流程控制的作用域解析和现有的流程解析很相似，比如对明确赋值(definite assignment)的检查，代码示例如下。\n1 2 3 4 5 6 7 8 9 10 11 private static int getDoubleLength(String s) { int a; // 声明但未赋值 if (s == null) { return 0; // 已返回 } else { a = s.length(); // 赋值 } // 已确定完成赋值 因此直接可用 a = a * 2; return a; } 相对于其它现代编程语言还是稍显啰嗦，例如kotlin无需声明模式变量，直接在原变量上调用方法。但实际上模式变量是确保向后兼容性的手段。改变obj instanceof String 中的类型也就意味着，在其被用作重载方法参数的时候，调用可能会被解析成这个方法的不同版本。\n1 2 3 if (obj is String) { print(obj.length) } 在该版本中预览了记录类和模式匹配组合使用的“记录类模式”，该模式匹配将在下文详细介绍。\nJava 17 (LTS) 封闭类 可用: 17-JEP 409: Sealed Classes (预览: 15-JEP 360: Sealed Classes (Preview); 16-JEP 397: Sealed Classes (Second Preview))\n封闭类用于限定哪些类或接口可以被用于继承或实现它们。之前的机制是final结合访问修饰符，标记为final的类不允许被继承，配合访问修饰符就能确保仅同一包中的类才能继承。封闭类提供更细粒度的控制，让开发者能显式地列举其子类。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // sealed声明封闭类，permits声明允许直接继承该封闭类的类型(实际上permits是要求必须直接继承封闭类) public sealed class Shape permits Circle, Quadrilateral, WeirdShape {} // final声明该类不允许再被继承 public final class Circle extends Shape {} public sealed class Quadrilateral extends Shape permits Rectangle, Parallelogram {} public final class Rectangle extends Quadrilateral {} public final class Parallelogram extends Quadrilateral {} // non-sealed声明该类可以被任意继承 public non-sealed class WeirdShape extends Shape {} 继承封闭类时，必须通过添加final/sealed/non-sealed显式定义出封闭类的边界，这也就使整条继承链的继承情况是明确定义的。\n被允许继承的类必须和父类（封闭类）在同一个包里，如果是使用java模块，那它们必须在同一模块中。\n如果类都比较简短，且大部分和数据相关，可以声明在同一个源文件，permits关键字可以忽略。\n1 2 3 4 5 6 7 8 9 10 public sealed class Shape { public final class Circle extends Shape {} public sealed class Quadrilateral extends Shape { public final class Rectangle extends Quadrilateral {} public final class Parallelogram extends Quadrilateral {} } public non-sealed class WeirdShape extends Shape {} } 记录类是隐式final的，可以作为封闭类的子类。\n推荐使用封闭类而非枚举\n，比如： java\nenum Expression { ADDITION, SUBTRACTION, MULTIPLICATION, DIVISION }\n在封闭类出现前，只能用枚举类对固定可选项建模。然而所有的情况都要在同一个源文件中写完，且枚举类仅支持常量，不支持需要实例的情况，例如表示一个类型的单个消息。\n封闭类提供一个比枚举类更好的选择，使得用普通类来为固定可选项建模成为可能。配合switch模式匹配就更能充分发挥其作用，封闭类能像枚举一样在switch表达式中使用，编译器能自动检查代码是否涵盖了全部情况。\n枚举类的值可以使用values方法列举出来。对应到封闭类和封闭接口，可以使用getPermittedSubclasses方法例举出所有被允许继承的子类。\nJava 21 (LTS) 1. switch模式匹配 可用: 21-JEP 441: Pattern Matching for switch; 预览: 17-JEP 406: Pattern Matching for switch (Preview); 18-JEP 420: Pattern Matching for switch (Second Preview); 19-JEP 427: Pattern Matching for switch (Third Preview); 20-JEP 433: Pattern Matching for switch (Fourth Preview)\nswitch表达式可以对任意类型进行复杂的模式匹配，以下通过示例演示。\nswitch支持常量模式匹配，与传统的表达式用法基本一致\n1 2 3 4 5 6 7 // 对枚举值进行匹配 var symbol = switch (expr) { case ADDITION -\u0026gt; \u0026#34;+\u0026#34;; case SUBTRACTION -\u0026gt; \u0026#34;-\u0026#34;; case MULTIPLICATION -\u0026gt; \u0026#34;*\u0026#34;; case DIVISION -\u0026gt; \u0026#34;/\u0026#34;; }; switch支持类型模式匹配，类似instanceof模式匹配\n1 2 3 4 5 6 return switch (expression) { case Addition expr -\u0026gt; \u0026#34;+\u0026#34;; case Subtraction expr -\u0026gt; \u0026#34;-\u0026#34;; case Multiplication expr -\u0026gt; \u0026#34;*\u0026#34;; case Division expr -\u0026gt; \u0026#34;/\u0026#34;; }; 模式还支持卫语句(guard)，写法为type pattern when guard expression\n1 2 3 4 5 String formatted = switch (o) { case Integer i when i \u0026gt; 10 -\u0026gt; String.format(\u0026#34;a large Integer %d\u0026#34;, i); case Integer i -\u0026gt; String.format(\u0026#34;a small Integer %d\u0026#34;, i); default -\u0026gt; \u0026#34;something else\u0026#34;; }; 这与instanceof模式匹配是一致的，代码如下所示。\n1 2 3 4 5 6 7 if (o instanceof Integer i \u0026amp;\u0026amp; i \u0026gt; 10) { return String.format(\u0026#34;a large Integer %d\u0026#34;, i); } else if (o instanceof Integer i) { return String.format(\u0026#34;a large Integer %d\u0026#34;, i); } else { return \u0026#34;something else\u0026#34;; } 模式变量作用域也类似，是分支敏感的。例如在case Integer i \u0026amp;\u0026amp; i \u0026gt; 10 -\u0026gt; String.format(\u0026quot;a large Integer %d\u0026quot;, i);语句中，变量i作用域是卫语句及其右侧的表达式。\nswitch支持匹配null值。传统的switch语句接受到null时会抛出空指针异常，switch表达式为保持后向兼容性，当没有显式声明null模式时也会抛出异常。\n1 2 3 4 5 switch (s) { case null -\u0026gt; System.out.println(\u0026#34;Null\u0026#34;); case \u0026#34;Foo\u0026#34; -\u0026gt; System.out.println(\u0026#34;Foo\u0026#34;); default -\u0026gt; System.out.println(\u0026#34;Something else\u0026#34;); } switch表达式必须是详尽的，要求覆盖所有可能输入。该约束与枚举、封闭类和泛型能很好协作。若只有一组固定输入，那便可以略去默认分支。这对维护代码有很大帮助，例如当给枚举新增常量，那么涉及该枚举的所有缺失默认分支的匹配都会在编译时抛出错误。详尽性检查是在编译时进行，但如果在运行时有新的实现（例如来自单独的编译），编译器还会插入一个默认分支去抛出MatchException。\n1 2 3 4 5 6 7 8 9 sealed interface I\u0026lt;T\u0026gt; permits A, B {} final class A\u0026lt;X\u0026gt; implements I\u0026lt;String\u0026gt; {} final class B\u0026lt;Y\u0026gt; implements I\u0026lt;Y\u0026gt; {} static int testGenericSealedExhaustive(I\u0026lt;Integer\u0026gt; i) { return switch (i) { case B\u0026lt;Integer\u0026gt; bi -\u0026gt; 42; }; } 以上代码来自JEP 441: Pattern Matching for switch，展示了模式匹配与封闭类和泛型的写作，代码能正确通过编译是因为编译器可以检测到只有A和B是I的有效子类型，并且由于泛性参数Integer，该参数只能是B\u0026lt;Integer\u0026gt;的实例。\n编译器也会执行与详尽性检查相反的操作，当编译器发现一个分支完全涵盖另一个分支时，会报出编译错误。代码示例如下。\n1 2 3 4 5 6 7 Object o = 1234; // 编译错误，第二个条件已包含在第一个条件分支中 String formatted = switch (o) { case Integer i -\u0026gt; String.format(\u0026#34;a small Integer %d\u0026#34;, i); case Integer i when i \u0026gt; 10 -\u0026gt; String.format(\u0026#34;a large Integer %d\u0026#34;, i); default -\u0026gt; \u0026#34;something else\u0026#34;; }; 出于代码可读性及正确性原因，强烈建议将常量匹配放在其相应的类型匹配之前，使更具体的分支先被匹配到。考虑以下代码，如果将常量匹配与带卫语句的类型匹配交换次序，当传入1时，会得到不同的结果。\n1 2 3 4 5 switch(num) { case -1, 1 -\u0026gt; \u0026#34;special case\u0026#34;; case Integer i when i \u0026gt; 0 -\u0026gt; \u0026#34;positive number\u0026#34;; case Integer i -\u0026gt; \u0026#34;other integer\u0026#34;; } 原文说的是编译器强制要求常量置前，实测并非如此。\n2. 记录类模式 可用: 21-JEP 440: Record Patterns (预览: 19-JEP 405: Record Patterns (Preview); 20-JEP 432: Record Patterns (Second Preview))\n记录类模式可以看作更强大的类型模式，在匹配时可以直接解构出对象的值(支持嵌套)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 interface Point { } record Point2D(int x, int y) implements Point { } record Point3D(int x, int y, int z) implements Point { } enum Color { RED, GREEN, BLUE } record ColoredPoint(Point p, Color c) { } // 构造对象 Point p1 = new ColoredPoint(new Point2D(3, 4), Color.GREEN); Point p2 = new ColoredPoint(new Point2D(1, 2, 3), Color.RED); // switch模式匹配解构对象 var length = switch (p) { case ColoredPoint(Point3D(int x, int y, int z), Color c) -\u0026gt; Math.sqrt(x*x + y*y + z*z); case ColoredPoint(Point2D(int x, int y), Color c) -\u0026gt; Math.sqrt(x*x + y*y); case ColoredPoint(Point p, Color c) -\u0026gt; 0; } // instanceof模式匹配也可以直接解构，实践中按需选择即可 if (p instanceof ColoredPoint(Point2D(int x, int y), Color c)) { // code } else if (p instanceof ColoredPoint(Point3D(int x, int y, int z), Color c)) { // code } 记录类模式简化了复杂的对象类型验证及字段提取工作，尤其是有嵌套对象的场景。\n3. 其它预览功能 JDK21中还有如下的预览语言功能，此处仅记录。待到下个LTS版本发布(Java 25 2025-09)再做详细整理。\n匿名模式和匿名变量 JEP 443: Unnamed Patterns and Variables (Preview) 字符串模板 JEP 430: String Templates (Preview) 未具名类和实例主方法 JEP 445: Unnamed Classes and Instance Main Methods (Preview) 近期终于能稍微抽身，做些“新潮”的东西，动手时与时代的脱节感愈发强烈，甚至感觉无从下手。 翻看Git仓库，上次正经整理博客是2020年，已经是五年前的事情了。这五年感觉像是遗失的梦一般，倏忽跳跃到现在，向前回忆却想不到闪光之处。懈怠多年，轻易挥霍时光，对自己感到遗憾和抱歉。 学习应当是终生坚持的事情，无论是编程还是其他。重新出发，充实自己，永不止步。\n","date":"2025-05-08T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/language/java-updates/","title":"Java语言功能更新"},{"content":" 假定客户端1.2.3.4访问服务5.6.7.8:5678，标记端口1234\n客户端规则 数据包修改 1 2 # 将访问{DST_IP}:{DST_PORT}的数据包中目的端口修改为{MARK_PORT} iptables -t nat -A OUTPUT -d {DST_IP} -p tcp --dport {DST_PORT} -j DNAT --to-destination :{MARK_PORT} 1 2 3 ## 示例 # 将访问5.6.7.8:5678的数据包中目的端口修改为1234 iptables -t nat -A OUTPUT -d 5.6.7.8 -p tcp --dport 5678 -j DNAT --to-destination :1234 路由跳转 将访问{DST_IP}的数据强制路由到动态代理服务(去进行后续的修改转发)\n1 2 3 4 5 6 7 8 # 查看当前路由规则 route -n # 获取默认网卡名称 ip -4 -o addr show | grep {HOST_IP} | awk \u0026#39;{print $2}\u0026#39; | head -n1 # 将访问特定IP的请求路由到动态代理服务(可以通过掩码修改为按段路由) route add -net {DST_IP} netmask 255.255.255.255 gw {DPS_IP} {NET_NAME} # 删除路由规则 route del -net {DST_IP} netmask 255.255.255.255 gw {DPS_IP} {NET_NAME} 服务端规则 启用规则 1 2 3 4 5 6 # 将访问{DST_IP}:{MARK_PORT}的数据包打上标记{MARK_PORT} iptables -t mangle -A PREROUTING -p tcp -d {DST_IP} --dport {MARK_PORT} -j MARK --set-mark {MARK_PORT} # 将被标记{MARK_PORT}的数据包转到实际目标端口{DST_PORT} iptables -t nat -A PREROUTING -m mark --mark {MARK_PORT} -p tcp -j DNAT --to-destination :{DST_PORT} # 将被标记{MARK_PORT}的数据包中的源IP修改为{SRC_IP} iptables -t nat -A POSTROUTING -m mark --mark {MARK_PORT} -j SNAT --to-source {SRC_IP} 假定客户端1.2.3.4访问服务5.6.7.8:5678，标记端口1234\n1 2 3 4 5 6 7 ## 示例 # 将访问5.6.7.8:1234的数据包打上标记1234 iptables -t mangle -A PREROUTING -p tcp -d 5.6.7.8 --dport 1234 -j MARK --set-mark 1234 # 将被标记1234的数据包转到实际目标端口5678 iptables -t nat -A PREROUTING -m mark --mark 1234 -p tcp -j DNAT --to-destination :5678 # 将被标记1234的数据包中的源IP修改为1.2.3.4 iptables -t nat -A POSTROUTING -m mark --mark 1234 -j SNAT --to-source 1.2.3.4 撤销规则 与启用规则的区别是参数-A/--append修改为-D/--delete\n1 2 3 iptables -t mangle -D PREROUTING -p tcp -d {DST_IP} --dport {MARK_PORT} -j MARK --set-mark {MARK_PORT} iptables -t nat -D PREROUTING -m mark --mark {MARK_PORT} -p tcp -j DNAT --to-destination :{DST_PORT} iptables -t nat -D POSTROUTING -m mark --mark {MARK_PORT} -j SNAT --to-source {SRC_IP} 查看规则 参数-S/--list-rules相对于-L/--list的输出更稳定且更适合程序处理\n1 2 3 4 5 6 # 查看`mangle`表`PREROUTING`链中的规则 iptables -t mangle -S PREROUTING # 查看`nat`表`PREROUTING`链中的规则 iptables -t nat -S PREROUTING # 查看`nat`表`POSTROUTING`链中的规则 iptables -t nat -S POSTROUTING 输出如下\n还包含其他规则、程序处理时必须严格过滤\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 iptables -V iptables v1.8.7 (nf_tables) ### iptables -t mangle -S PREROUTING -P PREROUTING ACCEPT -A PREROUTING -d 5.6.7.8/32 -p tcp -m tcp --dport 1234 -j MARK --set-xmark 0x4d2/0xffffffff ### iptables -t nat -S PREROUTING -P PREROUTING ACCEPT -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER -A PREROUTING -p tcp -m mark --mark 0x4d2 -j DNAT --to-destination :5678 ### iptables -t nat -S POSTROUTING -P POSTROUTING ACCEPT -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE -A POSTROUTING -s 172.19.0.0/16 ! -o br-a053f76f3ead -j MASQUERADE -A POSTROUTING -s 172.18.0.0/16 ! -o br-51b383a5369c -j MASQUERADE -A POSTROUTING -j LIBVIRT_PRT -A POSTROUTING -m mark --mark 0x4d2 -j SNAT --to-source 1.2.3.4 1 2 3 4 5 6 # 查看`mangle`表`PREROUTING`链中的规则 iptables -t mangle -L PREROUTING # 查看`nat`表`PREROUTING`链中的规则 iptables -t nat -L PREROUTING # 查看`nat`表`POSTROUTING`链中的规则 iptables -t nat -L POSTROUTING 清空规则 清空操作也会把其他应用的规则给删掉(例如Docker)，强烈不推荐\n1 2 3 4 5 6 # 清空`mangle`表`PREROUTING`链中的规则 iptables -t mangle -F PREROUTING # 清空`nat`表`PREROUTING`链中的规则 iptables -t nat -F PREROUTING # 清空`nat`表`POSTROUTING`链中的规则 iptables -t nat -F POSTROUTING 其他 1 2 # 查看iptables规则及过滤命令 iptables -t mangle -L PREROUTING --line-numbers | grep -E \u0026#39;^[[:digit:]]+[[:blank:]]+MARK\u0026#39; --- title: 数据链路图示 --- %%{init: {\u0026#34;flowchart\u0026#34;: {\u0026#34;htmlLabels\u0026#34;: false}} }%% flowchart TB subgraph 客户端链路 direction TB subgraph 容器内链路 direction TB A(浏览器) A--\u0026gt;|**容器IP-\u0026gt;目标IP:目标端口**|B subgraph 容器的iptables修改请求端口 direction RL B(容器的iptables) c1(将请求目标修改为 目标IP:标记端口) c1@{shape: comment} end end subgraph 容器主机的iptables路由请求 C(容器主机的iptables) c2(将访问目标IP的请求 路由到代理服务器) c2@{shape: comment} B--\u0026gt;|**容器IP-\u0026gt;目标IP:标记端口**|C end end subgraph 代理服务器的iptables修改请求 D(代理服务器的iptables) c3(代理服务器修改请求: 1. 将访问目标IP:标记端口的数据包打上特定的唯一标记 2. 将标记的数据包请求目标修改为目标IP:目标端口 3. 将标记的数据包请求源IP修改为用户主机IP 至此，原始请求源IP被修改为用户IP，隐藏容器的存在) c3@{shape: comment} C--\u0026gt;|**容器IP-\u0026gt;目标IP:标记端口**|D end subgraph 目标服务器 E(目标服务) D--\u0026gt;|**用户IP-\u0026gt;目标IP:目标端口**|E end ","date":"2025-04-25T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/snippet/iptables-command/","title":"iptables修改数据包源地址"},{"content":"Limits and Colimits Limits and Colimits\nCategory Theory II 1.2: Limits Category Theory II 2.1: Limits, Higher order functors Category Theory II 2.2: Limits, Naturality Category Theory II 3.1: Examples of Limits and Colimits\n极限和余极限\n在范畴论中，所有事物之间都是有联系的，我们可以从任意角度对它们进行考察。例如对积的泛构造，现在我们已经了解了函子和自然变换，那么这个泛构造是否可以简化甚至泛化？下面尝试一下\n积的构造始于对两个对象a和b的选择，我们想要构造这二者的积。首先，“选择对象”到底意味着什么？我们能否用范畴论中的术语来描述这一动作？两个对象形成了一个非常简单的模式，我们可以将这个模式抽象到一个非常简单的范畴。我们将这个范畴称为2，它只包含了1和2两个对象，除了两个必须存在的恒等态射，也没有其他态射了。现在可以将“从范畴C中选择两个对象”的行为表述为“定义一个从范畴2到范畴C的函子D”。一个函子将对象映射到对象，因此它的象只是两个对象（也可能是一个，若该函子映射对象时有坍缩行为的话）；函子也映射态射，在当前情况下只是将恒等态射映射成恒等态射。\n这种方式的优点它建立在范畴概念上，避免了“选择对象”这种不确切描述。此外，它也很容易概括，因为很容易就能用比2更复杂的范畴来定义模式。\n继续，定义一个积的下一步便是对候选对象c的选择。再次对“选择”换个说法，将之描述成从单例范畴出发的函子。（实际若是用Kan extensions来表述就好多了，但尚未接触这一概念，因此这里用了一点小技巧）从范畴2到范畴C的常量函子Δ。\nHere again, we could rephrase the selection in terms of a functor from a singleton category. And indeed, if we were using Kan extensions, that would be the right thing to do. But since we are not ready for Kan extensions yet, there is another trick we can use: a constant functor Δ from the same category 2 to C. The selection of c in C can be done with Δc. Remember, Δc maps all objects into c and all morphisms into idc.\n","date":"2020-09-03T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-2/2.limit-and-colimit/","title":"2.2 Limits and Colimits - 端和余端"},{"content":"Category Theory and Declarative Programming Category Theory and Declarative Programming\nCategory Theory II 1.1: Declarative vs Imperative Approach\n第一部分中，讨论了范畴论和编程都是关乎组合的概念。编程过程中，将一个大问题解构成多个子问题，再将这些子问题解构成规模更小的子问题，最终子问题的粒度足够细，从而变成人类能够直接解决的小问题；所有的小问题都解答完毕之后，再将小问题的结果组合起来，一层层向上回溯，最终解决这一原始的、规模较大的问题。一般而言，总是会存在两种编程方式：告诉电脑“做什么/(what to do)”、或者告诉电脑“怎么做/(how to do)”。前者称之为命令式/imperative，后者称为声明式/declarative\n考虑一个实例，复合操作本身就可以从声明式和命令式两种途径给出定义。假定函数h是函数g及f的复合，声明式的写法是h = g . f；而命令式h x = let y = f x in g y。命令式编程定义了一系列动作，这些动作严格按顺序进行，在函数f计算完成之前，对g的调用绝对不可能发生（从代码的概念设计上如此，在某些惰性计算、按需调用的参数传递的语言中，实际执行情况可能存在一些不一致）。\n事实上，基于编译器的工作，以上两个版本的代码在实际执行时可能仅有一点差别甚至并无区别。在写出既能解决问题、又有较强维护性和可测试性的代码的过程中，背后的方法论可能完全不一样。\n主要问题在于：当我们面对一个难题的时候，是否总是可以在命令式或声明式两种方法中自由选择？若是存在一个声明式风格的解决方案，是否总是可以将之转换成代码实现？这一问题似乎有些偏离主题了，但若能找到答案，很可能会再次革新我们对宇宙万物的理解。\n现在通过解释一个物理学概念来演示解决问题的两种方式。物理学中，大部分法则的解释都有两种方式，一种是从局部角度考虑、或者无穷小角度。观测一个规模很小的系统状态，预测在下一个时刻系统状态的变化，通常用微分方程表述，通过对一段时间进行积分或求和等，得出系统状态\n这种方式便体现了命令式思想：通过一步步逼近从而到达最终状态，每一步都依赖上一步的结果。事实上，计算机对物理系统的模拟就是将微分方程转换为差分方程，然后进行迭代。宇宙飞船射击类游戏中，飞船的运动便是这样计算出来的。随时间跳动，飞船的坐标都是在进行增量累加，位置增量是由速度和时间得出的，而速度是由初速度、加速度和时间得出，加速度又是由力和质量得出。这一系列方程背后便是牛顿运动定律。对于更复杂的问题，也可以用同样的思想来解答，例如用麦克斯韦方程组来研究电磁场的传播，用晶格QCD（量子色动力学）研究质子内部夸克和胶子的行为等。\n数字计算机鼓励了时空离散化与局部化思想的结合。斯蒂芬·沃尔夫拉姆（Stephen Wolfram）曾尝试将整个宇宙的复杂性简化为一个元胞自动机系统，这一行为极致地体现了两者结合。\n另一种办法是从全局考虑。观察系统的起始状态和最终状态，然后通过某个函数计算链接这两个状态的最短轨迹。\n最简单的例子是费马最小时间原理，它指出光线是沿着传播时间最短的路径传播。一般而言，没有反射和折射发生时，光线从A点到达B点的必定是走的最短路径，即直线路径。光线在透明介质中的传播是比真空中要慢的，若是光线起点位于空气中，止点位于水中，那么尽量让光在空气中跑远一点，在水中跑近一点，这样整体传播时间才更短。所有的经典力学都可以从最小作用原理导出。通过积分动势能之差的拉格朗日函数，可以计算任何轨迹。例如发射一枚炮弹并击中目标，炮弹首先向上飞直到重力势能最大点，然后再转向下飞，势能转化成动能。费马的这些贡献甚至可以用于解释量子机制，这一原理只是关注起始和最终状态，然后计算中间轨迹。\n从上看出，在解释物理定律时有两种对偶的办法，可以基于一个局部视角、事件按序发生、事件间进行某些增量的叠加；也可以基于一个全局视角，声明起始状态和结束状态，两状态之间的所有东西就在各自时空位置上安静待着。\n全局视角的思考方式也确实可以用在编程上。考虑实现一个光线传播程序，声明光源和观测点，那么就直接可以计算其传播路径。实现过程中并没有显式进行“最短时间”、“最短路径”等相关设置，但是隐含的还是用到了相关物理定理和几何知识。\n这两种看待问题的方式最大的不同在于它们对于空间以及更重要的时间的处理方式。局部视角更关注“此时此地”，而全局视角是一个静态视角的长期观测，好像未来是确定的，我们只是在分析某个永恒宇宙的性质。\n用函数响应式编程/Functional Reactive Programming来编写用户交互程序时也特别体现全局视角的优越性。常见编程模型是对于所有可能出现的用户输入编写对应的事件处理程序，这些事件处理程序之间可能修改一组共享的可变状态。FRP编程模型外部事件视为一个无限列表，然后在这个列表上应用一系列变换，从概念上讲，所有未来可能出现的动作就在那摆着，等着作为输入进入程序。从程序本身的视角看，这些事件是无序且随机的，对于所有情况，若想得知第N项的状态，那必须先处理完前N-1项。当应用于时序型事件时，我们称之具备因果关系。\n范畴论鼓励我们尽可能采用全局视角来看待问题，因此也就是在鼓励声明式编程。首先，范畴论不像是微积分那样有距离、附近、极限、时间等概念，我们所拥有的全部东西就是抽象的对象和对象间的态射。若是能通过一系列步骤从A到达B，那么必定存在从A直接到B的办法。此外，范畴论中的主要工具就是泛构造，泛构造本身也是全局视角看待和解决问题的一个例子。\n我们曾用泛构造来定义积，通过声明积的性质即完成。积是一个伴随着两个投影态射的对象，它是个“最好”的对象，因为它能将其他对象及投影进行分解。相对应地，传统的笛卡尔积定义就相当命令式了，需要描述清楚如何从一个集合中选取一个元素、从另一个集合选取一个元素，然后将这两个元素构造成一个序对，这个序对便是积的一个实例。此外，还需要定义解构的方法。\n当然，几乎所有的编程语言都直接内置了积、余积、函数类型等概念，而不是通过泛构造来定义（当然也有范畴式编程语言对此进行尝试）。不管是否直接使用，范畴论中的定义都可以证明已有的编程结构是合理的，并且可以产生新的结构。最重要的是，最重要的是，范畴理论提供了一种元语言，用于在声明层次上对计算机程序进行推理。它还鼓励在将问题规范转换为代码之前对其进行推理。\n","date":"2020-08-30T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-2/1.category-theory-and-declarative-programming/","title":"2.1 Category Theory and Declarative Programming - 范畴论和声明式编程"},{"content":"Scala Style 参照官方文档推荐的Scala代码风格SCALA STYLE GUIDE\n缩进和换行 该部分参考INDENTATION\n每个缩进级别都是2个空格\n尽量保证每行不要超过80字符，若有表达式超出了，尽量考虑换行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 缩进是2个空格 object Foo { def bar(): Unit = { val x = 10 //... } } // --- // 超长表达式的换行 val result = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 // 超多参数的函数调用，变量名超长时要注意方法在新一行开始调用 val myLongFieldNameWithNoRealPoint = foo( someVeryLongFieldName, andAnotherVeryLongFieldName, \u0026#34;this is a string\u0026#34;, 3.1415) 命名约定 该部分内容参考NAMING CONVENTIONS\n绝大多数情况下，Scala采用小驼峰风格lowerCamelCase，一些缩写术语也作为普通单词对待xHtml/maxId，由于下划线在Scala语法中有特殊意义，因此非常不推荐使用（若要强行使用、编译器也不会主动拒绝此类代码）\n类、特质 类和特质的命名应当遵循大驼峰风格，跟Java中对于类的命名约定是一致的\n有时类或特质以及它们的成员是用于描述格式、文档或者某些协议，此时为了保持与输出一模一样，可以不遵循对类名和成员的命名约定。但应注意只能在这种特定用途使用，不能影响其余代码\n对象 对象一般也遵循大驼峰命名风格，但是当之功能上模仿包或者函数的时候，可以不遵循这一约定\n1 2 3 4 5 6 7 8 9 10 11 12 // 功能类似一个名为`ast`的包 object ast { sealed trait Expr case class Plus(e1: Expr, e2: Expr) extends Expr ... } // 功能类似一个名为`inc`的函数 object inc { def apply(x: Int): Int = x + 1 } 包 对包的命名应当遵循Java的命名约定，例如com.foo.bar\n有时会出现必须用_root_指定包的全名的情况。但应注意不要过度使用_root_，以相对包路径方式来引用嵌套包是很推荐的做法，此外这十分有助于简化import语句\n方法 Scala中，对于普通文本/字母名称的方法应当采用小驼峰风格\nAccessors/Mutators 属性访问器、修改器 其功能可以直接对应成Java中常用的getter/setter，但是命名风格与Java完全不一致，Scala中采用一下约定\n对于属性的访问器/Accessors，该方法的名称与属性名称保持完全一致\n某些情况下，对于Boolean类型的访问器，允许在前面加上is（例如isEmpty），这种情况下也要保证没有相应的修改器才行。（Lift框架中，对此种情况的约定是在属性名后加_?，这是非标做法，注意不要滥用该做法）\n对于修改器/Mutators，其方法名应当是在属性名后加_=。遵循该约定后，调用点对属性的赋值将映射为调用该修改器方法（这不再仅是个约定，而变成了语言的约束）\n1 2 3 4 5 6 7 8 9 10 11 12 class Foo { def bar = ... def bar_=(bar: Bar) { ... } def isBaz = ... } val foo = new Foo foo.bar // accessor foo.bar = bar2 // mutator foo.isBaz // boolean property 由于Java没有对于属性及其绑定的一等支持，因此有了getter/setter范式的存在。在Scala中，有专门的库用于做此类支持\n1 2 3 class Company { val string: Property[String] = Property(\u0026#34;Initial Value\u0026#34;) // 一个不变的属性对象引用，但属性对象保存的值是可以修改的 } 无参方法/函数的括号 定义和调用无参方法/函数时，应当注意并考虑清楚是否要带括号\n1 2 def foo1() = ... def foo2 = ... 以上两个方法在Scala中都是合法的，两者也是不同的。其中foo2只能无括号方式调用、但foo1可以带括号调用。\n实际行为类似访问器的方法、而且方法调用无任何副作用，那么声明时不应当带括号。若有副作用，那么就应当带上括号。调用时也应当遵循此约定，若是要调用含副作用的方法，注意带上其括号\n符号方法名 尽量避免！\nScala语言库中符号用得很多，但实际编程中对于符号的使用还是应当慎重考虑，尤其是这一符号没有其标准意义时。两个合理的使用场景是：DSL中(actor ! msg)；数学操作符（a + b c :: d）。在标准的API设计领域，严格限制符号方法名只能使用在纯函数式操作中。可以定义\u0026gt;\u0026gt;=来进行单子的bind操作，但是不允许定义\u0026lt;\u0026lt;方法向输出流写东西。因为前者是有完备数学定义的，而且没有任何副作用，但后者既没有标准定义也不是无副作用的操作。\n一般而言，能以符号命名的方法应当是广为人知或者能很好地自描述的。一旦需要解释这个方法到底在做什么，那么该方法就不适合用符号名称了。\n常量、值、变量和方法 常量命名应采用大驼峰风格，例如scala.math.Pi；一般的值、变量和方法应采用小驼峰命名风格\n类型参数（泛型） 简单的类型参数一般用一个大写字母即可，一般从A开始\n1 2 3 class List[A] { def map[B](f: A =\u0026gt; B): List[B] = ... } 若是类型参数有特定意义，那么可以用具备描述性的单词，该单词遵循类型的命名规则（注意不是全大写）；当然，若是的类型参数的作用域足够小，直接用单个字母依然没有问题\n1 2 3 4 5 6 7 8 9 10 11 // 可以用单词 class Map[Key, Value] { def get(key: Key): Value def put(key: Key, value: Value): Unit } // 也可以用字母 class Map[K, V] { def get(key: K): V def put(key: K, value: V): Unit } HKT 理论上讲，高阶类型参数跟一般类型参数没什么不同（当然高阶kind至少是* =\u0026gt; *而不是简单的*）。一般而言，更倾向于使用更具描述性的单词，而不是一个简单的字母，例如class HigherOrderMap[Key[_], Value[_]] { ... }。对于一些基础概念，可以用简单的字母，例如F[_]来表述函子、M[_]表述单子。\n注解 Scala中注解一般采用小驼峰\n特别说明 由于Scala本质上是函数式编程语言，因此def add(a: Int, b: Int) = a + b这种名称短小、方法体简单的函数定义很常见。这种行为在Java中是个很不好的做法，但是在Scala中是很推荐的。\n类型 该部分内容参考TYPES\n类型推断 在保证代码清晰的前提下，尽可能地用上类型推断的功能。在向外开放API的地方，注意显式给出类型信息。\n对于私有字段或本地变量，几乎完全不用显式给出类型，因为它们的类型我们一般能直接从值上看出来，对于那些不那么明显的、或者比较复杂的，还是应该显式给出类型。对于所有的公共成员，必须明确给出类型信息。\n特别地，Scala编译器对于函数值/λ表达式的类型推断做了特殊处理，对于需要传入函数的高阶函数调用，可以不必声明该表达式参数的类型。考虑ints.map(i =\u0026gt; i * 2)，编译器可以直接推断出λ表达式的参数i的类型。\n类型注解 类型注解该以如下方式写value: Type，例如i: Int、d: Double甚至l: ::\nType ascription / 类型归属？ 类型归属的语法跟类型注解是一致的，所以很容易搞混，例如Nil: List[String] None: Option[String] \u0026quot;Str12\u0026quot;: AnyRef Set(values: _*)。类型归属是在向编译器声明我们期望该值的类型是什么，一般在做上转型操作，前三者例子便是如此；但Scala中更常见的可能是最后一例，当调用一个可变参数的方法时、期望将序列展开，因此用到_*（否则会变成可变参数列表仅接收到一个参数，该参数是个序列）\n函数 函数的类型遵循(argType1, argType2, ...) =\u0026gt; retType的写法。特别地，对于元数为1的函数，可以省去参数的括号，写作argType =\u0026gt; retType\n结构类型 若是结构类型的总长度小于50字符，那么应该写在一行里，否则就应写作多行，并且为之分配一个类型别名。\n1 2 3 4 5 6 7 8 9 10 // 简单结构类型 def foo(a: { val bar: String }) = ... // 复杂结构类型 private type FooParam = { val baz: List[String =\u0026gt; String] def bar(a: Int, b: Int): String } def foo(a: FooParam) = ... 当把简单结构类型写在一行的时候，多个成员之间应该用一个分号和空格隔开，成员跟花括号之间也应有空格\n结构类型是在运行时用反射实现的，因此性能较差。开发时还是应尽可能选择常规类型，除非结构类型能带来明显的益处\n嵌套代码块 NESTED BLOCKS\n代码块 左花括号必须跟它所归属定义的声明放在同一行\n括号 当某表达式跨越多行且需要小括号包裹时，左右两小括号与其包裹的内容之间应当没有空格、并且与内容保持同一行；\n1 2 (this + is a very ++ long * expression) 小括号也可用于禁用分号推断，因此允许更喜欢将操作符写在开头的开发者写出如下代码\n1 2 3 4 ( someCondition || someOtherCondition || thirdCondition ) 声明 DECLARATIONS\n类 类、对象、特质的构造器应当尽可能放在一整行里，当这一行过长（比如超出100字符）的时候，那么需要将该行拆成多行，每个构造器参数及其跟随的逗号占一行，如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 单行形式 class Person(name: String, age: Int) { … } // 多行形式 class Person( name: String, age: Int, birthdate: Date, astrologicalSign: String, shoeSize: Int, favoriteColor: java.awt.Color, ) { def firstMethod: Foo = … } 若是该类、对象、特质扩展了其他成员，采用同样的规则。尽可能放在一行里，若是单行超出100字符，那就拆成如下的多行形式\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Person( name: String, age: Int, birthdate: Date, astrologicalSign: String, shoeSize: Int, favoriteColor: java.awt.Color, ) extends Entity with Logging with Identifiable with Serializable { def firstMethod: Foo = … } 类中各元素的顺序 类、对象、特质中的所有成员之间一般都应当用一个空行隔开。对于val和var，如果定义足够简单（比如单行少于20字符）并且没有Scala Doc，那么多个val或var之间可以不加空行。\n一般而言，字段定义应当在方法之前，但当字段求值是多行表达式的时候，此时字段实际上有点方法的味道了（例如在List上计算其长度）。这样的字段可以在文件靠后的位置，注意，仅限val和lazy val使用这一后放规则。注意千万不能把var在类里面放得遍地都是\n方法 方法应当以def foo(bar: Baz): Bin = expr形式进行声明，若是方法参数有默认值，应当在等号两侧加空格。注意，应当对于所有公有成员声明返回类型，既能显式描述方法返回类型，也能避免编译器推断的返回类型在实现修改后发生变化，从而导致二进制不兼容。本地方法或私有方法可以不显式给出返回类型\n过程式语法 编程实践中尽量不要用过程式语法\n1 2 3 4 5 6 7 8 9 // don\u0026#39;t do this def printBar(bar: Baz) { println(bar) } // write this instead def printBar(bar: Bar): Unit = { println(bar) } 方法修饰符 对方法的修饰符应当按以下顺序。注解、每个注解单独一行；重写标识符override；访问控制修饰符protected、private；隐式关键字implicit；final关键字；def关键字\n方法体 若是方法体只是一行简单表达式，那么不必用花括号包裹起来，若该表达式比较短（比如少于30字符），那么跟方法定义直接写在一行即可；该表达式比较长的话，那么就新起一行。具体选择哪种比较主观，总体思想是使代码可读性更好，若是方法声明很长但表达式很短，那也应当写在新一行里，避免使声明行过长。\n对于仅有match表达式的方法，xx match {应与方法声明写在同一行。\n方法体是多行表达式的时候，应注意必须用花括号。\n多参数列表 应当仅在有着充分理由的时候使用该语言功能，一般而言，有以下三方面原因驱使编写该风格代码：Fluent API设计、隐式参数、为了更好的类型推断\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 自定义流程控制API def unless(exp: Boolean)(code: =\u0026gt; Unit): Unit = if (!exp) code unless(x \u0026lt; 5) { println(\u0026#34;x was not less than five\u0026#34;) } // 对于多参数列表，给出第一个参数后，其余参数可以用编译器类型推断来简化编写 // scala中，一般这样定义 def fold[U](unit: U)(op: (U, A) =\u0026gt; U): U // 这样调用 List(\u0026#34;\u0026#34;).fold(0)(_ + _.length) // 若是这样定义 def fold[U](unit: U, op: (U, A) =\u0026gt; U): U // 只能这样声明具体类型的调用 List(\u0026#34;\u0026#34;).foldLeft[Int](0, _ + _.length) // 下面这种调用是不对的 List(\u0026#34;\u0026#34;).fold(0, (i: Int, s: String) =\u0026gt; i + s.length) 对于复杂DSL或者类型名称超长的类型，可能很难在一行中完整声明整个函数签名，这种情况可以每个参数列表占一行，左括号对齐\n高阶函数 在Scala中，只要在声明函数的时候稍微留心，那么调用高阶函数的时候可以有一些语法上的简化。考虑fold函数，SML中它的签名fun fold (f: ('b * 'a) -\u0026gt; 'b) (init: 'b) (ls: 'a list)，在Scala中，一般是相反的参数顺序def fold[A, B](ls: List[A])(init: B)(f: (B, A) =\u0026gt; B): B = ...。把函数参数放在最后一个，那么调用该函数的时候，可以用上函数简化语法fold(List(1,2,3,4))(0)(_+_)\n字段 字段遵循的声明规则跟方法大约一致。此外，对于lazy的字段，val必须紧跟lazy关键字\n函数值 Scala允许多种语法声明函数值，例如，以下几种方式都是正确的\n1 2 3 4 val f1 = ((a: Int, b: Int) =\u0026gt; a + b) val f2 = (a: Int, b: Int) =\u0026gt; a + b val f3 = (_: Int) + (_: Int) val f4: (Int, Int) =\u0026gt; Int = (_ + _) 其中，第一种和第四种方式是建议写法，第二种相对也还行，但是当函数体多行的时候容易出现问题；第三种是最简洁的，但是可能比较难以理解，尤其是一眼看上去可能看不懂，得想一下才能懂什么意思。\n函数值中，在小括号和它们包裹代码之间不应当有空格，但花括号与其代码见应用空格。\n大部分函数可能不像上例给出的那样简要，它们可能都是有多行表达式。这种情况下，就将函数切分到多行去，这种情况下必须用第一种风格\n1 2 3 4 val f1 = { (a: Int, b: Int) =\u0026gt; val sum = a + b sum } 此外，函数值的声明和调用都应尽可能发挥编译器类型推断的功能。\n控制结构 CONTROL STRUCTURES\n所有的控制结构关键词if/for/while之后都必须紧跟一个空格\n花括号 当控制结构表达的是一个纯函数式操作并且各个分支都只是单行表达式时应当省略花括号\nif表达式：若是存在else分支，则应当省略花括号；否则必须用花括号包裹起来，哪怕只有单行表达式 while：必须带上花括号（因为它永远不可能描述一个纯函数式操作） for：若是有yield语句，那么可以省略花括号；否则必须用花括号包裹循环体，哪怕仅有一行表达式 case: 省略花括号 1 2 3 4 5 6 7 8 9 10 11 12 13 val news = if (foo) goodNews() else badNews() if (foo) { println(\u0026#34;foo was true\u0026#34;) } news match { case \u0026#34;good\u0026#34; =\u0026gt; println(\u0026#34;Good news!\u0026#34;) case \u0026#34;bad\u0026#34; =\u0026gt; println(\u0026#34;Bad news!\u0026#34;) } for-Comprehensions 在for语句中可以写多个生成器（即多个\u0026lt;-）对于存在yield语句的for表达式，当有多个生成器的时候，要用花括号包裹且每个生成器占一行；仅有一个生成器的时候，用小括号包裹，如下\n1 2 3 4 5 6 7 8 9 10 11 12 // right for (i \u0026lt;- 0 to 10) yield i // wrong! for (x \u0026lt;- board.rows; y \u0026lt;- board.files) yield (x, y) // right! for { x \u0026lt;- board.rows y \u0026lt;- board.files } yield (x, y) 存在特例，对于那些没有yield的for表达式，这种情况属于普通的循环而不是函数式操作，此时使用小括号包裹一个或多个生成器\n1 2 3 4 5 6 7 8 9 10 11 12 // wrong! for { x \u0026lt;- board.rows y \u0026lt;- board.files } { printf(\u0026#34;(%d, %d)\u0026#34;, x, y) } // right! for (x \u0026lt;- board.rows; y \u0026lt;- board.files) { printf(\u0026#34;(%d, %d)\u0026#34;, x, y) } for-comprehensions可能会常倾向于链接map、flatMap、filter等调用，但是会导致代码可读性很差，因此这种情况要尽可能用增强的for表达式\n细碎的条件判断 有些需要三元操作符?/:的场景，scala中无这样的操作符，但是可以直接用简单的if/else表达式来表述，例如val res = if (foo) bar else baz。要注意这种风格不要在命令式运用if/else时使用\n方法调用 METHOD INVOCATION\n简要来讲，Scala中的方法调用遵循Java风格的约定。在调用对象、点、方法名之间没有空格，方法名和参数列表之间没有空格，参数之间应当以逗号和一个空格隔开。\nScala 2.8开始支持了命名参数，进行方法调用时，命名参数整体应当看作一个普通参数（即逗号加一个空格分隔），命名参数自身等号两侧应当各有一个空格\n1 2 3 4 5 foo(42, bar) target.foo(42, bar) target.foo() foo(x = 6, y = 7) 0元函数/无参函数 Scala允许省略0元函数调用时的括号。当要调用的方法没有任何副作用的时候才能用这种语法，否则必须带上括号。\n1 2 3 4 5 xx.toString() // √ xx.toString // √ println() // √ println // x Postfix Notation/后缀写法 此外，对于无参数函数，Scala也允许采用后缀写法，但是要尽可能避免，这里只是给出了有这种写法，但是非常不建议使用。仅限在某些DSL中才允许用这种写法。\n1 2 names.toList names toList // x 1元函数/单个参数 - 中缀写法 对于这类函数，Scala允许一种完全无需任何符号的语法，即中缀写法。通常情况下，应当注意避免这种语法，但当方法名是个符号参数部分是个函数的时候可以用，当然也必须保证是在纯函数的环境中用。\n1 2 3 names.mkString(\u0026#34;,\u0026#34;) // √ names mkString \u0026#34;,\u0026#34; // 有时会见到这种写法，但这种写法存在争议，要避免 javaList add item // x 符号方法、操作符 这种方法/函数必须要以中缀风格来进行调用。\n1 2 3 4 5 6 \u0026#34;daniel\u0026#34; + \u0026#34; \u0026#34; + \u0026#34;spiewak\u0026#34; // √ a + b // √ \u0026#34;daniel\u0026#34;+\u0026#34; \u0026#34;+\u0026#34;spiewak\u0026#34; // x a+b // x a.+(b) // x 绝大多数情况下，遵循Java和Haskell中的约定。在某些灰色地带中，对那些方法名短小、实际效果类似操作符的函数，尤其是当它满足了交换律的时候，例如max，x max y这种写法也是相当常见的。\n符号方法可能接受多个参数，这种情况下要用中缀语法来调用，foo ** (bar, baz)。这种方法其实相当少了，在设计API的时候应注意，尽可能避免设计出这样的API。Scala的集合API设计中存在/:和:\\，尽量不要使用它们，转而使用更具意义的方法名foldLeft和foldRight。\n高阶函数 对于那些接受一个函数为参数的方法，应尽量使用的中缀语法。但是用中缀语法的时候不要带着多余的符号了\n1 2 3 names.map { _.toUpperCase } // x names.map { _.toUpperCase }.filter { _.length \u0026gt; 5 } // √ names map { _.toUpperCase } filter { _.length \u0026gt; 5 } // √ 文件 FILES\n一般来讲，文件中应当只包含逻辑上的单个组件。所谓“逻辑上”的单个组件，指的是一个类型、特质或对象。当类或特质有其伴生对象时，要将伴生对象放在类或特质的同一文件中。文件名应当就是类、特质或对象的名称。此外，应当按照其包的结构放在相同目录结构中。\nScala放宽了对于文件名、目录结构的限制，但实际编程中还是应注意遵循Java风格的约定。\n1 2 3 4 5 6 package com.novell.coolness class Inbox { ... } // companion object object Inbox { ... } 多元素文件 有些很特殊的情况就是要违反以上约定。\n一个很常见的例子便是对于封闭抽象类、封闭特质及其子类的实现（一般是对ADT相关功能的实现）。因为封闭类、特质从语言级别就约束了其子类必须与其在同一个源文件中。\n1 2 3 4 5 sealed trait Option[+A] case class Some[A](a: A) extends Option[A] case object None extends Option[Nothing] 另一个常见场景是多个类从逻辑上很内聚，它们基于同样的一组前提或者概念，为方便维护便放在了同一个源文件中。这种情况确实存在，但应仔细斟酌是否放到一个源文件中\n所有的多元素文件都必须以小驼峰给文件命名\n例如option.scala、ast.scala\n文档 SCALADOC\n应当为所有的包、类、特质、方法及其他成员提供详细的文档。Scala文档遵循Java的文档风格，此外也提供了更丰富的功能用于简化文档的书写。\n应当更关注文档的主旨和内容，而不是格式。文档以简短的概括开始，然后再给出更深入的细节描述。一下给出几种常见风格\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // ---- Java doc 风格 ---- /** * Provides a service as described. * * This is further documentation of what we\u0026#39;re documenting. * Here are more details about how it works and what it does. */ def member: Unit = () // ---- Scala doc 风格 （星号在第二列对齐） ---- /** Provides a service as described. * * This is further documentation of what we\u0026#39;re documenting. * Here are more details about how it works and what it does. */ def member: Unit = () // ---- Scala doc 风格 （星号在第三列对齐） ---- /** Provides a service as described. * * This is further documentation of what we\u0026#39;re documenting. * Here are more details about how it works and what it does. */ def member: Unit = () // ---- 某些特别简单的注释 ---- /** Does something very simple */ def simple: Unit = () SCALADOC FOR LIBRARY AUTHORS\n","date":"2020-07-16T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/language/scala/scala-style/","title":"Scala Style"},{"content":"Spark 相关内容随记 随手记录Spark相关的问题、思考等\nSpark SQL在100TB上的自适应执行实践\nUser Defined Aggregate Functions (UDAFs)\nSpark SQL - DataSource 通过实现Spark定义的DataSource接口为Spark新增自定义数据源\n数据源API目前分V1和V2版本，到目前为止Spark 3.0.0似乎还没有完成进化，已在3.0.0版本完成V2版重构\nData source V2 API refactoring\n预计将在3.2.0版本将V2版API稳定下来\nStabilize Data Source V2 API\nhttps://jaceklaskowski.gitbooks.io/mastering-spark-sql/spark-sql-data-source-api-v2.html\nhttps://jaceklaskowski.gitbooks.io/mastering-spark-sql/spark-sql-DataSourceV2.html\nhttps://jaceklaskowski.gitbooks.io/mastering-spark-sql/spark-sql-DataSource.html\nCategory: datasource-v2-series\nCategory: datasource-v2-spark-three\nSpark SQL - CSV CSV类型文件中，出于各种原因可能导致Spark SQL解析数据会出错。\n以下问题举例在Hadoop2.6.0-Spark2.1.1-Scala2.10.6-JDK1.7生产环境出现，较新版本中的Spark具体行为暂不可知。该Spark版本已被魔改且无代码，离线环境中只有Spark2.4.4-Scala2.11，尝试看下源代码发现该部分已被重构，抛异常的类都没有了\n例如，有些字段里面包含了特殊字符，导致Spark SQL解析行数据时出现了字段截断错误，从而导致列错位，有些转换函数直接执行失败，进而导致整个任务失败。\n问题解决方式是强制指定mode=DROPMALFORMED，直接将问题数据丢弃，这是Spark SQL直接支持的配置，看文档的时候可能看到了，但是无视掉了。。。\nSpark文档中对于CSV支持的配置有详细介绍。\n最新版本的参考文档：DataFrameReader#csv\nSpark 2.4.6参考文档：DataFrameReader#csv\nSpark CLI 要脱离灵活性太差的自研任务调度服务、逐渐开始习惯用原生CLI进行进行任务的提交\nspark-submit --name JOB-NAME --master yarn --deploy-mode cluster --conf spark.yarn.submit.waitAppCompletion=false --class com.mxtao.App --jars /xxx/xxx.jar,/xxx/xxxx.jar --queue xx --pincipal xxx@DOMAN --keytab xxx.keytab main-class-in-this-jar.jar args-for-main\nSubmitting Applications\nRunning Spark on YARN - Spark Properties\nSubmitting Applications\nRunning Spark on YARN - Spark Properties\n","date":"2020-07-04T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/platform/spark/spark-notes/","title":"Spark相关内容随记"},{"content":"Higher Kinded Type - 高阶类类型 Kind 维基百科：Kind (type theory)\n\u0026ldquo;kind\u0026quot;中文翻译也是“类型”，这样就跟\u0026quot;type\u0026quot;有些混淆。下文仅将“type”翻译类型。\nKind是类型理论(Type Theory)中的定义。一个kind便是一个类型构造器的类型(the type of a type constructor)，或者说是高阶类型类型操作符的类型(the type of a higher-order type operator)。\nKind System本质上是简单类型Lambda演算(simply typed lambda calculus)的“上一层”。定义一个原始类型(primitive type)(记作*/$*$、称之为“type”)，它是任意数据类型(data type)的kind，不需要任何类型参数。\nkind有个可能看上去有些莫名其妙的解释：“(数据)类型的类型”（type of a (data) type），但实际上它也有些元数指示的意思。从语法上讲，可以很自然地将多态类型视为类型构造器，因此非多态类型可以视为一个0元类型构造器/无参类型构造器，所有的无参类型构造器都是相同的、最简单的kind，便是*/$*$\n高阶类型操作符在编程语言中并不常见，在大多数编程实践中，kind用于区分数据类型和用于实现参数化多态的构造器类型(types of constructors which are used to implement parametric polymorphism)。在类型系统使得用户能编码实现参数化多态的编程语言中(如C++ Haskell Scala)，或明或暗地有kind的概念存在。\nExamples */$*$是所有数据类型的kind，读作\u0026quot;type\u0026rdquo;，也可看作0元类型构造器/无参类型构造器，在当前上下文中也称为恰当类型(proper type)，通常也包含函数式编程语言中的函数类型。例如Int/Bool/List\u0026lt;Int\u0026gt;/Map\u0026lt;String, List\u0026lt;Double\u0026gt;\u0026gt;的kind都是* * -\u0026gt; */$* \\rightarrow *$是一元类型构造器(unary type constructor)的kind。例List类型的kind便是* -\u0026gt; *，需要给出一个类型参数Int才能得到恰当类型List\u0026lt;Int\u0026gt; * -\u0026gt; * -\u0026gt; */$* \\rightarrow * \\rightarrow *$是二元类型构造器(binary type constructor，curry方式实现)的kind。例如Tuple便是二元类型构造器；函数类型构造器-\u0026gt;也属此类（注：对-\u0026gt;应用的结果是函数类型，函数类型kind是*） (* -\u0026gt; *) -\u0026gt; */$(* \\rightarrow *) \\rightarrow *$是从一元类型构造器到恰当类型的高阶类型操作符的kind Values Types Kinds 1 Int * [1,2,3] List\u0026lt;Int\u0026gt; * (1, \u0026quot;\u0026quot;) Tuple\u0026lt;Int, String\u0026gt; * fun i -\u0026gt; i % 2 == 0 Int -\u0026gt; Bool * - List * -\u0026gt; * - Tuple * -\u0026gt; * -\u0026gt; * - -\u0026gt; * -\u0026gt; * -\u0026gt; * 从左向右提升抽象层次\nKind In Haskell Haskell的Kind系统中，$K = * | K \\rightarrow K$，它描述了两条规则：*/$*$是所有数据类型的kind；* -\u0026gt; */$* \\rightarrow *$是一元类型构造器的kind，接受一个kind然后给出一个kind。\n确实有值的类型才是算作具体类型，或者叫恰当类型。例如，4是Int类型的值、[1, 2, 3]是[Int]类型的值、fun i -\u0026gt; i % 2 == 0是Int -\u0026gt; Bool类型的值、fun x -\u0026gt; fun y -\u0026gt; x % y == 0是Int -\u0026gt; Int -\u0026gt; Bool类型的值。这些类型的kind都是*。\n一个类型构造器接受一个或多个类型参数，当接受了足够的类型参数之后便产生了一个新类型(类型构造支持基于currying的偏应用)。例如，[]/List接受一个类型参数，这个类型参数指出了内部元素的类型，因此[Int]/[Bool]/[[Int]]都是对于[]的正确应用，[]的kind是* -\u0026gt; *，Int/Bool/[Int]的kind是*，将之应用到[]便得到[Int]/[Bool]/[[Int]]，这些结果类型的kind是*。同理，二元组类型构造器(,)的kind是* -\u0026gt; * -\u0026gt; *，三元组类型构造器(,,)的kind是* -\u0026gt; * -\u0026gt; * -\u0026gt; *\nHKT in Programming 静态类型语言中，类型系统来保证值的使用安全性，kind系统是来保证类型的使用安全性。\n主流编程语言中提供的一般是一阶参数化多态(first-order parametric polymorphism)，这一功能一般称为泛型(generic)。泛型能对类型进行抽象并进行静态检查，但无法对类型构造器进行抽象，进而无法保证这方面类型安全(不支持HKT的话也写不出来，因为编译器不接受这样的代码，只能舍弃抽象，从而导致代码重复)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // ---------- Version 1 ---------- trait Iterable[T] { def filter(p: T =\u0026gt; Boolean): Iterable[T] def remove(p: T =\u0026gt; Boolean): Iterable[T] = filter(x =\u0026gt; !p(x)) } trait List[T] extends Iterable[T] { def filter(p: T =\u0026gt; Boolean): List[T] = ??? override def remove(p: T =\u0026gt; Boolean): List[T] = filter(x =\u0026gt; !p(x)) } // ---------- Version 2 ---------- trait Iterable[T, Container[_]] { def filter(p: T =\u0026gt; Boolean): Container[T] def remove(p: T =\u0026gt; Boolean): Container[T] = filter(x =\u0026gt; !p(x)) } trait List[T] extends Iterable[T, List] 以上代码用Scala简单演示了引入HKT带来的收益，确实减少了不必要的代码重复，而对于类型安全的保证也不会有任何损失。下面是个稍复杂些的例子，用Haskell和Scala对函子进行定义和实现，从而对HKT进行演示。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- Functor in Haskell class Functor f where fmap :: (a -\u0026gt; b) -\u0026gt; f a -\u0026gt; f b -- data Maybe a = Nothing | Just a instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just x) = Just (f x) -- data List a = Nil | Cons a (List a) instance Functor List where fmap _ Nil = Nil fmap f (Cons x xs) = Cons (f x) (fmap f xs) -- `(-\u0026gt;)` is a type constructor. `(-\u0026gt;) r a` =\u0026gt; `r -\u0026gt; a` -- here: `fmap :: (a -\u0026gt; b) -\u0026gt; (r -\u0026gt; a) -\u0026gt; (r -\u0026gt; b)` instance Functor ((-\u0026gt;) r) where fmap f g = f . g 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // Functor in Scala trait Functor[F[_]] { def fmap[A, B](f: A =\u0026gt; B)(fa: F[A]): F[B] } object OptionFunctor extends Functor[Option] { def fmap[A, B](f: A =\u0026gt; B)(a: Option[A]): Option[B] = { a match { case None =\u0026gt; None case Some(x) =\u0026gt; Some(f(x)) } } } object ListFunctor extends Functor[List] { def fmap[A, B](f: A =\u0026gt; B)(list: List[A]): List[B] = { list match { case Nil =\u0026gt; Nil case head :: tail =\u0026gt; f(head) :: fmap(f)(tail) } } } class FunctionFunctor[R] extends Functor[({type λ[A] = R =\u0026gt; A})#λ] { def fmap[A, B](f: A =\u0026gt; B)(g: R =\u0026gt; A): R =\u0026gt; B = { f compose g } } 分析以上代码，定义函子Functor用到了参数f/F[_]，该参数是个类型构造器，其kind便是* -\u0026gt; *，于是函子Functor的kind便是(* -\u0026gt; *) -\u0026gt; *，这里便是kind的“高阶”所在。此外要注意函数类型构造(-\u0026gt;)，对于其返回类型呈(协变)函子性，因此可以固定函数参数类型为r/R，然后可实现r -\u0026gt; ?/R =\u0026gt; ?函子。Scala中需要显示指出类型构造器，Haskell可以通过对于参数的应用推断出这是个类型还是个类型构造器。\n论文Generics of a Higher Kind解释了Scala对于HKT的设计及应用演示，论文中给出了一个更具实用意义的例子，通过对Iterable基础类库的设计和实现，演示了HKT存在的必要性（上文已简要演示）。此外也简要解释了Scala的“implicit”设计理念，该设计以另一种方式提供了Haskell的Type Class提供的特设多态(ad-hoc polymorphism)功能。此外，Scala的Kind System中，kind还附带着类型的上下界、可变性的约束信息，以此来确保程序的正确性。\nF#尚不支持HKT，需要CLR改进才能支持这一功能，对功能的讨论在：Simulate higher-kinded polymorphism。Robert Kuzelj的Higher Kinded Types in F#系列博客演示了HKT存在的必要性以及在F#中如何对其进行模拟。\nHigher Kinded Types in typescript博客介绍了在TypeScript中对HKT功能进行模拟。\nRust对HKT的似乎也开始支持，高阶类型 Higher Kinded Type对此进行了简单介绍，看评论似乎不是HKT完整支持。\n","date":"2020-05-28T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/language/higher-kinded-type/","title":"Higher Kinded Type - 高阶类类型"},{"content":"范畴 / Category 一些事物（称为对象/object）及事物之间的关系（称为态射/morphism）构成一个范畴。\n最小的范畴是拥有0个对象的范畴。因为没有对象，自然也就没有态射。\n可以通过用态射将对象连接起来的方法构造出范畴。\n给出一个有向图，将它的结点视为对象，将节点间的箭头视为态射。在这个有向图上增加箭头，就可以将之构造成范畴。首先为每个结点添加恒等箭头，然后为所有首位相邻的箭头（即符合复合条件）增加复合箭头。注意每次新增一个箭头，必须考虑它本身与其它箭头（除了恒等箭头）的复合。这种由给定的图产生的范畴，称为自由范畴。以上是一种自由构造的示例，即给定一个结构，用符合法则（在此，就是范畴论法则）的最小数量的东西来扩展它。\n编程语言中，一般是类型体现为对象，函数体现为态射。\n复合 / Composition 范畴的本质是复合，或者说复合的本质是范畴。若有从对象A到对象B的态射，也有从对象B到对象C的态射，那么必定存在从对象A到对象C的复合态射。\n数学中，一般以 $g \\circ f$ 表示函数复合（复合顺序从右向左，即 $g \\circ f = \\lambda x.g \\lparen f \\lparen x \\rparen \\rparen$，可读作“g after f”）。下以几种函数式编程语言进行复合思想的演示\n1 2 3 4 5 6 7 -- Haskell -- Haskell中，小写字母表示类型参数，具体类型是大写字母开头 f :: a -\u0026gt; b -- 接受a返回b的函数 g :: b -\u0026gt; c -- 接受b返回c的函数 -- 用`.`符进行复合，同数学写法一致，从右向左 g . f -- f g 复合，其签名 `a -\u0026gt; c` 1 2 3 4 5 6 7 8 9 10 // F# // F# 中无 `Bottom Type` 的概念。F#中以`\u0026#39;`作为前缀的类型名称视为类型参数 let f : \u0026#39;a -\u0026gt; \u0026#39;b = fun x -\u0026gt; failwith \u0026#34;not implement\u0026#34; // let f\u0026lt;\u0026#39;a, \u0026#39;b\u0026gt; : \u0026#39;a -\u0026gt; \u0026#39;b = failwith \u0026#34;not implement\u0026#34; -- 另一种方式 let g : \u0026#39;b -\u0026gt; \u0026#39;c = fun x -\u0026gt; failwith \u0026#34;not implement\u0026#34; // 用内置操作符`\u0026gt;\u0026gt;` `\u0026lt;\u0026lt;`进行复合，前者从左向右，后者从右向左 f \u0026gt;\u0026gt; g // 从左向右复合 g \u0026lt;\u0026lt; f // 从右向左复合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // Scala // Scala 中 `???` 是个 `Nothing` 类型值，`Nothing` 是Scala中的\u0026#34;Bottom Type\u0026#34; // Scala中的方法/Method 函数/Function 函数值/Function Value 概念存在区别和联系 // 方法强调的是“实例方法”，是在“对象实例”上进行调用的。在trait、class中以`def`关键字定义的有参值是方法（Scala模糊了无参方法和属性的界限，无参方法可以不带括号调用求值） // 函数无需任何实例即可调用，一般只能进行形如`func(arg)`的调用求值操作。在object、函数中以`def`关键字定义的可视为函数 // 函数值可视为一个函数实例，可以调用其实例方法。以λ给出的值、函数经`func _`转换的值、方法经`obj.method _`转换的值都是函数值，可将该值绑定到`funcVal`上 // `funcVal.apply(arg)` 传入参数求值（可以直接这样用`funcVal(arg)`，会转换成对`apply`方法的调用） // `funcVal.compose(funcVal\u0026#39;)` 传入另一个函数值，进行函数复合，其顺序从右向左 // `funcVal.andThen(funcVal\u0026#39;)` 传入另一个函数值，进行函数复合，其顺序从左向右 // ... // 注：`andThen` `compose`方法混入自`Function1`特质 (仅`Function1`特质定义了此方法，换言之，仅单参数列表、单参数函数才能进行复合) // // Scala中的函数和方法可以是参数多态/泛型的，但函数值必定是确定类型的 // Scala中函数不是自动柯里化/Currying，只能以定义多参数列表函数的形式进行显式柯里化（λ表达式写出柯里化不需任何特殊处理） // def func(x: A)(y: B)(z: C): X = ??? // val func = (x: A) =\u0026gt; (y: B) =\u0026gt; (z: C) =\u0026gt; ??? // val func: A =\u0026gt; B =\u0026gt; C =\u0026gt; X = x =\u0026gt; y =\u0026gt; z =\u0026gt; ??? val f : A =\u0026gt; B = ??? val g : B =\u0026gt; C = ??? g compose f // 从右向左复合，方法作为中缀操作符形式 g.compose(f) // 从右向左复合，实例方法调用形式 f andThen g // 从左向右复合，方法作为中缀操作符形式 f.andThen(g) // 从左向右复合，实例方法调用形式 态射的复合需要满足以下两个性质\n结合律 / associative\n复合要满足结合律。若有三个态射 $f$ $g$ $h$ 可被复合（即对象可被首尾相连），必有 $h \\circ \\lparen g \\circ f \\rparen$ = $\\lparen h \\circ g \\rparen \\circ f$ = $h \\circ g \\circ f$。下面编程语言中以伪代码演示该性质（编程语言未定义“函数相等”）\n1 2 3 4 5 -- Haskell f :: A -\u0026gt; B g :: B -\u0026gt; C h :: C -\u0026gt; D h . (g . f) == (h . g) . f == h . g . f 1 2 3 4 5 6 // F# let f : \u0026#39;a -\u0026gt; \u0026#39;b = fun x -\u0026gt; failwith \u0026#34;not implement\u0026#34; let g : \u0026#39;b -\u0026gt; \u0026#39;c = fun x -\u0026gt; failwith \u0026#34;not implement\u0026#34; let h : \u0026#39;c -\u0026gt; \u0026#39;d = fun x -\u0026gt; failwith \u0026#34;not implement\u0026#34; h \u0026lt;\u0026lt; (g \u0026lt;\u0026lt; f) == (h \u0026lt;\u0026lt; g) \u0026lt;\u0026lt; f == h \u0026lt;\u0026lt; g \u0026lt;\u0026lt; f // 从右向左复合 f \u0026gt;\u0026gt; (g \u0026gt;\u0026gt; h) == (f \u0026gt;\u0026gt; g) \u0026gt;\u0026gt; h == f \u0026gt;\u0026gt; g \u0026gt;\u0026gt; h // 从左向右复合 1 2 3 4 5 6 // Scala val f : A =\u0026gt; B = ??? val g : B =\u0026gt; C = ??? val h : C =\u0026gt; D = ??? h compose g compose f // 从右向左复合 f andThen g andThen h // 从左向右复合 对于函数复合的结合律，以上演示应当还是比较显而易见，但是在其它范畴中，可能结合律并不是那么显然。\n恒等态射 / identity morphism\n范畴中的任一对象，都存在恒等态射。这个态射从自身出发又返回自身。它是复合的最小单位。恒等态射与任何（符合复合条件的）态射复合，得到的都是后者自身。恒等态射称为$id_A$（意为 identity on A，即A与自身恒等）。\n在数学中，若有接受$A$返回$B$的函数$f$，则有$f \\circ id_A = f$ 和 $id_B \\circ f = f$\n编程语言中的实现很简单，只需要简单地把输入返回即可（一般函数式编程语言标准库中已有该基本元素）\n1 2 3 4 5 6 7 8 -- Haskell id :: a -\u0026gt; a -- `id` 的函数签名，接受任意类型并返回此类型 id x = x -- `id` 的定义 注：Haskell标准库（称为Prelude）已定义 f :: a -\u0026gt; b -- 它与恒等函数复合之后还是其自身， 注：Haskell中是从右向左复合 id . f == f -- 这里的 `id` 是 `id_b` f . id == f -- 这里的 `id` 是 `id_a` 1 2 3 4 5 6 7 8 9 10 11 12 13 // F# // FSharp.Core 中 `id` 的定义、并以特性方式注明了编译成程序集之后的名称 [\u0026lt;CompiledName(\u0026#34;Identity\u0026#34;)\u0026gt;] let id x = x // F#具备自动泛化/Automatic Generalization特性，其类型是 `id : \u0026#39;a -\u0026gt; \u0026#39;a` let f : \u0026#39;a -\u0026gt; \u0026#39;b = fun x -\u0026gt; failwith \u0026#34;not implement\u0026#34; id \u0026gt;\u0026gt; f == f // 从左向右复合，此处 `id` 是 `id_a` f \u0026gt;\u0026gt; id == f // 从左向右复合，此处 `id` 是 `id_b` id \u0026lt;\u0026lt; f == f // 从右向左复合，此处 `id` 是 `id_b` f \u0026lt;\u0026lt; id == f // 从右向左复合，此处 `id` 是 `id_a` 1 2 3 4 5 6 7 8 9 10 11 12 // Scala // scala-library 中 `identity` 的定义，标注了 `@inline` @inline def identity[A](x: A): A = x val f : a =\u0026gt; b = ??? (identity[b] _) compose f == f // 从右向左复合，此处 `identity` 是 `identity_b` f compose identity[a] == f // 从右向左复合，此处 `identity` 是 `identity_a` f andThen identity[b] == f // 从左向右复合，此处 `identity` 是 `identity_b` (identity[a] _) andThen f == f // 从左向右复合，此处 `identity` 是 `identity_a` 函数 / Function 数学上的函数是值到值的映射。在编程语言中，可以实现数学上的函数：一个函数，给它一个输入值，它就计算出一个结果。每次调用时，对于相同输入，总能得到相同的输出。\n编程语言中，给出相同输入保证得到相同输出，且对外界环境无关的函数，称为纯函数。纯函数式语言Haskell中，所有函数都是纯的。对其它语言，可以构造一个纯的子集，谨慎对待副作用。之后将会看到单子如何只借助纯函数对副作用进行建模。\nSet Set是集合的范畴。在Set中，对象是集合，态射是函数。\n存在一个空集 $\\emptyset$，它不包含任何元素；也存在只有一个元素的集合。函数可以将一个集合中的元素映射到另一个集合；也能将两个元素映射为一个。但是函数不能将一个元素映射成两个。恒等函数可以将一个元素映射为本身。\n类型 / Type 范畴中，并非任意两个态射皆可复合。当某态射的源与另一态射的目标是同一对象时，两态射才能复合。在编程语言中，类型关乎复合。在参数及返回类型上，两函数必须满足复合条件，这样在类型意义上程序才是安全的（当然，这一般仅是程序通过编译的保证，并非逻辑正确的保证）。\n对于类型，一个直观理解是：类型是值的集合。例如，Bool类型是2个元素True False的集合，Char类型是所有Unicode字符的集合。集合可能是有限的（例如Bool），也可能是无限的（例如String）。当声明x :: Integer时，是在说x是整型数集中的一个元素。\n理想世界中，可以说Haskell的数据类型是集合，Haskell的函数是集合之间的数学函数。但由于“数学函数只知道答案，不可被执行”，Haskell必须要计算才能得出答案。若是可以在有限步骤内计算完毕，这没有什么问题，但有些计算是递归的，可能永远不会终止。在Haskell中无法阻止无终止的计算（停机问题）。Haskell为每个类型添加了一个特殊值，称为底/Bottom，用符号表示为_|_或⊥。这个值与无休止计算有关。若一个函数声明为f :: a -\u0026gt; Bool，它可以返回True False或_|_，后者表示它不会终止。\n将底作为类型系统的一部分之后，可以将运行时错误作为底对待，甚至可以允许函数显式地返回底（一般用于未定义表达式）。例如声明f :: a -\u0026gt; Bool 定义f x = undefined。因为undefined求值结果是底，它可以是任何类型的值，因此该定义可以通过类型检查。（甚至f = undefined，因为底也是a -\u0026gt; Bool这种类型的值）。\n可以返回底的函数称为偏函数；全函数则可以保证对任意参数返回有效的结果。\n由于底的存在，Haskell的类型与函数的范畴称为Hask而不是Set。从实用的角度看，可以暂时无视掉这些无终止的函数与底，将Hask视为一个友善的Set即可。\n注：Scala中也有底类型的概念存在，Nothing是任何类型/Any的子类型，Null是所有引用类型/AnyRef的子类型。F#中没有这一概念。\n基于类型是集合的直觉，思考一些特殊情形。\n空集\n在Haskell中，空集是Void，这是个没有任何值的类型。可以定义一个接受Void的函数，但是无法调用它。因为无法提供一个Void类型的值（这种值不存在）。该函数的返回值没有任何限制，这是个多态返回类型的函数。Haskell中该函数称为absurd :: Void -\u0026gt; a。这种类型与函数，在逻辑学上有更深入的解释。Void表示谎言，absurd函数的类型相当于“由谎言可以推出任何结论”，这也就是逻辑学中的“爆炸原理”。\nF# 和 Scala 中似乎没有 空集/Void 的概念 ？\n单例集合\n这是只有一个值的类型。它实际上是其它编程语言如C++/Java中常见的void类型。考虑一个函数int f42() {return 42;}。已知无法调用不接受任何值的函数，函数f42被调用时发生了什么？从概念上说，接受了一个空值。由于不会有第二个空值，所以没有显式强调它。在Haskell中为空值提供了一个符号()（读作\u0026quot;unit\u0026quot;，该符号既是类型也是值）\nF#中，空值的类型为unit，值为()；Scala中空值类型为Unit，值为()，它是AnyVal的子类型。\n注意，每个接受unit的函数都等同于从目标类型中选择一个值的函数。实际上，可以将f42作为数字42的另一种表示方法，这也演示了如何通过与函数交互来代替显式给出集合中某个元素。这证实了数据与计算过程在本质上是没有区别的。从unit到类型A的函数就相当于集合A中的元素。\n考虑让函数返回一个unit的情形。在C++等其它编程语言中，这样的函数通常担当含有副作用的函数，这并非数学意义上的函数。一个返回unit类型的纯函数，它什么也不做；或者说，唯一做的就是丢弃接受的输入。\n1 2 3 -- Haskell 中的 `unit` 函数 unit :: a -\u0026gt; () unit _ = () 1 2 3 4 // F# 中的 `ignore` 函数 // ignore : \u0026#39;a -\u0026gt; unit [\u0026lt;CompiledName(\u0026#34;Ignore\u0026#34;)\u0026gt;] let inline ignore _ = () Scala中好像没有类似的函数\n二元集合\n二元集合一般对应编程语言中的布尔类型。命令式/面向对象编程语言中，该类型一般是内置的bool。但在Haskell中可以自行定义 data Bool = True | False（读作Bool要么是True要么是False）。（F#中也可以直接进行定义type Bool = True | False，Scala中需要借助封闭抽象类/sealed abstract class来定义）\n接受Bool的纯函数只是从目标类型中选择了两个值，一个关联了True，另一个关联了False。返回Bool的函数被称为“谓词/predicate”。\n注： 二元集合并非只有Bool，自定义类型也可能是个二元集合（例如type Gender = Male | Female）\nHom-集 / Hom-Set 在一个范畴C中，从对象a到对象b的态射集称为hom-集，记作C(a,b)或$Hom_C\\lparen a,b \\rparen$。\n在集合范畴中，hom-集自身也是个对象，因为它也是个集合，这种称为内hom-集，如左图所示；对于其它范畴，hom-集只是个范畴之外的集合，这种被称为外-hom集，如右图所示\n序 / Order 存在这样的范畴，其中态射描述两个对象之间的小于等于关系（这是个范畴。每个对象都小于等于自身，因此恒等态射存在；若$a \\le b$且$b \\le c$，则$a \\le c$，态射复合存在；另，态射复合遵守结合律）。伴随这种关系的集合称为前序集/preorder，一个前序集是一个范畴。\n可以加强这种对象间的关系，要求该关系满足一个附加条件，即，若$a \\le b$且$b \\le a$，则必有$a = b$。伴随这种关系的集合称为偏序集/partial order。\n若一个集合中的任意两个元素之间存在偏序关系，这种集合称为全序集/total order。\n可将这些有序集描绘为范畴。前序集所构成的范畴，在任意两个对象之间最多只有一个态射，这样的范畴称为瘦范畴。瘦范畴内的每个hom-集要么是空集，要么是单例/singleton。在任意前序集构成的范畴内，C(a,a)也是个单例hom-集，只包含恒等态射。前序集中是允许出现环的，但偏序集中不允许。\n排序需要用到前序、偏序和全序的概念。例如快排、归并之类的排序算法，只能在全序集中进行；偏序集可以用拓扑排序来进行处理。\n幺半群 / Monoid 幺半群是个简单且重要的概念，它是基本算术幕后的概念：只要有加法或乘法运算就可以形成幺半群。编程中幺半群有很多实例，表现为字符串、列表、可折叠数据结构、并发编程中的future、函数式响应编程中的事件等。\n数学上，幺半群$\\langle S, *, e \\rangle$是指一个带有可结合二元运算($*: S \\times S \\rightarrow S$，这隐含了$S$对运算$*$封闭)和单位元$e$的代数结构$S$。“可结合”是指二元运算满足结合律，$\\forall a,b,c \\in S \\Rightarrow \\lparen a * b \\rparen * c = a * \\lparen b * c \\rparen$；单位元是指，$\\exists e \\in S \\And \\forall a \\in S \\Rightarrow a * e = e * a$\nSet伴随笛卡尔积运算便构成幺半群，其幺元是单例；Set伴随“不交并/Disjoint Union”运算也构成幺半群，其幺元是空集。\n个人理解：Cat中，对象是范畴，态射是函子。其构成幺半群所需的二元运算即为“二元函子”。\n作为集合\n伴随着一个满足结合律的二元运算和一个特殊“中立”元素的的集合被称为幺半群。对与该二元运算，这个“中立”元素的行为类似一个返回其自身的“unit”。\n例如，加法运算和包含0的自然数集便形成一个幺半群。结合律是指(a+b)+c=a+(b+c)或$\\lparen a + b \\rparen + c = a + \\lparen b + c \\rparen$。这个理想的、永远保持“中立”的元素是0，因为0+a=a以及a+0=a（$0+a=a$ $a+0=a$）。由于加法满足交换律（a+b=b+a $a+b=b+a$），所以似乎再强调a+0=a有点多余。但应注意，交换律并非幺半群所需。例如，字符串连接运算不遵守交换律，但字符串及其连接运算可以构成幺半群，它的中立元素是空字符串。\n作为范畴\n幺半群可被描述为带有一个态射集的单对象范畴，这些态射皆符合复合规则。\n只含单个对象m的范畴M存在hom-集M(m,m)。在这个集合上可以定义一个二元运算，M(m,m)中两元素“相乘”相当于两态射的复合。复合总是存在的，因为这些态射的源对象与目标对象是同一个对象。这种“乘法运算”也符合范畴论法则中的结合律，因为态射复合满足结合律。恒等态射也是肯定存在的。因此，总是能够从幺半群范畴中复原出幺半群集合。因此，幺半群范畴与幺半群集合是同一个东西。\n在范畴论中，是在尝试放弃查看集合及其元素，转而讨论对象和态射。因此，现从范畴的角度来看作用于集合的二元运算。\n例如，一个将每个自然数都加5的运算（会将0映射为5、将1映射为6等等）这样就在自然数集上定义了一个函数，现在有了一个函数与一个集合。通常对于任意数字n，都会有一个加n的函数，称之为“adder”。把这些“adder”采用符合直觉的方式去复合，例如adder5与adder7的复合式adder12。因此“adder”的复合等同于加法规则，现在可以用函数的复合来代替加法运算。此外，还有一个面向中立元素0的adder0，它不会改变任何东西，因此它是自然数集上的恒等函数。\n每个范畴化的幺半群都会定义一个唯一的伴随二元运算的集合的幺半群，事实上总是能够从单个对象的范畴中抽出一个集合，这个集合是态射的集合。\n1 2 3 4 5 6 7 8 9 10 11 -- Haskell -- 定义 `Monoid` 类型类 class Monoid m where empty :: m append :: m -\u0026gt; m -\u0026gt; m -- currying form -- 将 `String` 声明为一个 `Monoid` ，提供 `empty` `append` 的实现 instance Monoid String where empty = \u0026#34;\u0026#34; append = (++) -- 中缀运算符用括号包住后，就转化为接受两个参数的函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 // F# // F# 中不存在 类型类/type class 的概念，参考Scala版本尝试给出了使用接口进行的定义 module Monoid = open System // `String`类存在于`System`名称空间下，或者可以直接用F#为之给出的类型别名：`string`，此处是为了形式一致 type Monoid\u0026lt;\u0026#39;T\u0026gt; = // F# 中定义泛型类型时，写法可以是`type \u0026#39;T Monoid`或`type Monoid\u0026lt;\u0026#39;T\u0026gt;` abstract member Zero: \u0026#39;T abstract member BiOp: (\u0026#39;T -\u0026gt; \u0026#39;T -\u0026gt; \u0026#39;T) // 注意，此处给出`BiOp`的类型`(\u0026#39;T -\u0026gt; \u0026#39;T -\u0026gt; \u0026#39;T)`时带上了括号 let stringMonoid = { // 使用对象表达式，直接实例化一个实现了接口的匿名类对象。 new Monoid\u0026lt;String\u0026gt; with member _.Zero = \u0026#34;\u0026#34; member _.BiOp = (+) } type StringMonoid = // 似乎基于对接口的实现来定义一个新的类并不合适，似乎不太符合\u0026#34;Monoid\u0026#34;的意义 interface Monoid\u0026lt;String\u0026gt; with // 也可以写作`String Monoid` member _.Zero = \u0026#34;\u0026#34; member _.BiOp = (+) // --------------------------- // F#是一门混合范式语言，一般是函数式编程范式优先。涉及到与面向对象范式混合编程时，存在一些注意事项 // 在函数式世界里，函数是一等公民，可以作为值来用。 // `fun x -\u0026gt; x + 1` 是一个匿名函数值，与数字`1` 字符串`\u0026#34;abc\u0026#34;`的身份没有高低之分 // `let add1 = fun x -\u0026gt; x + 1` 是在将“函数值”绑定到名称`add1`上，与`let a = 1`在做的事情没有区别 // `let add\u0026#39; = add1` 是在将`add1`的值绑定到名称`add\u0026#39;`上，与`let b = a`行为类似 // 面向对象的世界里函数/方法并非一等公民，是有特殊处理的 type I\u0026lt;\u0026#39;T, \u0026#39;R\u0026gt; = // 定义一个泛型接口 abstract member M1: \u0026#39;T -\u0026gt; \u0026#39;R // 没有括号，这是在定义接受`\u0026#39;T`返回`\u0026#39;R`的函数 abstract member M2: (\u0026#39;T -\u0026gt; \u0026#39;R) // 带上括号，这是在定义`FSharpFunc\u0026lt;\u0026#39;T,\u0026#39;R\u0026gt;`(`\u0026#39;T -\u0026gt; \u0026#39;R`)类型的只读属性 type C\u0026lt;\u0026#39;T, \u0026#39;R\u0026gt;() = // 定义一个泛型类型 let f:\u0026#39;T-\u0026gt;\u0026#39;R = failwith \u0026#34;err\u0026#34; // 私有的函数类型字段 interface I\u0026lt;\u0026#39;T, \u0026#39;R\u0026gt; with // 该类实现泛型接口 member _.M1 t = f t // 实现接口中定义的方法，必须显式给出参数。调用时使用`obj.M1(x)`方式 member _.M2 = f // 实现接口中定义的属性，只能直接赋函数值。这是`FSharpFunc\u0026lt;\u0026#39;T,\u0026#39;R\u0026gt;`类型的属性，注意与`Func\u0026lt;T,R\u0026gt;`类型不同 // --------------------------- // 使用Monoid实例的时候，似乎可以考虑使用静态解析类型参数来进行约束，而不必要求必须是某个接口的实现 ?? // 尝试实现失败 // Constraints: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/constraints // Statically Resolved Type Parameters: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters // Type extensions: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/type-extensions // 静态解析的类型参数: https://docs.microsoft.com/zh-cn/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters // 只有在定义时或内部类型扩展(Intrinsic type extensions)给出的成员才是符合静态类型约束的 // 类型扩展: https://docs.microsoft.com/zh-cn/dotnet/fsharp/language-reference/type-extensions#optional-type-extensions // todo: 等到F#5发布，扩展成员可以作为满足约束的证据 // 参考ISSUE：[RFC FS-1043] Extension members visible to trait constraints #6805： https://github.com/dotnet/fsharp/pull/6805 // RFC： https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1043-extension-members-for-operators-and-srtp-constraints.md 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 object Monoid { trait Monoid[M] { def zero : M def biOp : (M, M) =\u0026gt; M } val stringMonoid = new Monoid[String] { def zero = \u0026#34;\u0026#34; def biOp = _ + _ } class StringMonoid extends Monoid[String] { def zero = \u0026#34;\u0026#34; def biOp = _ + _ } } // ------------------------- trait T[A, B] { // 定义一个泛型特质，该特质将会被编译成java interface val value: A // 在特质中定义一个A类型的值，将会被编译成接口中一个返回A的无参方法 def p: A =\u0026gt; B // 在特质中定义一个`A=\u0026gt;B`类型的值，将被编译成一个返回`A=\u0026gt;B`/`Function1[A,B]`的无参方法 def m(a: A): B // 在特质中定义一个接受A返回B的方法，将被编译成一个普通的接口方法 } 注：概念上 append = (+) 与 append s1 s2 = (+) s1 s2 是不同的。前者是Hask范畴（忽略\u0026quot;Bottom Type\u0026quot;的话则是Set）中态射的相等，这样不仅写法简洁，也经常被泛化到其它范畴。后者称为外延相等/extensional equality，陈述的是对任意两个输入，append 与 (+) 的值是相同的。由于参数的值有时也被称为point（函数$f$在点$x$出的值），外延相等也被称为point-wise相等，未指定参数的函数相等称为point-free相等。\n同构 / Isomorphism 数学上，意味着存在一个从对象a到对象b的映射，同时也存在一个东对象b到对象a的映射，这两个映射是互逆的。在范畴论中，用态射取代映射。一个同构是一个可逆的态射；或者是一对互逆的态射。\n可以通过复合与恒等来理解互逆：若态射g与态射f的复合结果是恒等态射，那么g是f的逆。这体现为以下两个方程(因为两个态射存在两种复合形式) f . g = id g . f = id\n初始对象 / Initial Object 推广对象的“序”的概念，若存在一个从对象a到对象b的态射，那么认为对象a比对象b更靠前。在范畴中，若存在一个对象，它发出的态射指向所有其它对象，那么这个对象就是初始对象。对于给定范畴，可能无法保证初始对象的存在；也可能存在很多个初始对象。考虑有序范畴，在有序范畴中，任意两个对象之间，最多只允许存在一个态射。基于此，给出初始对象定义\n初始对象：有且仅有一个态射指向范畴中任意一个对象。\n以上定义主要是为了让初始对象在同构意义下具备唯一性。(意思是任意两个初始对象都是同构的。)\n假设两个初始对象$i_1$ $i_2$。由于$i_1$是初始对象，因此有唯一的态射f从$i_1$到$i_2$；同理也有唯一的态射从$i_2$到$i_1$。考虑这这两个态射的复合，$g \\circ f$必定是$i_1$到$i_1$的态射，由于$i_1$是初始对象，只允许一个$i_1$到$i_1$态射的存在。在范畴中，$i_1$到$i_1$态射是恒等态射，因此$g \\circ f$等同于恒等态射。同理，$f \\circ g$也是恒等态射。这样就证明了f与g互逆，因此两个初始对象是同构的。\n例如，偏序集的初始对象是那个最小的对象。但是，有些偏序集（如整数集）没有初始对象。集合范畴中，初始对象是空集。Haskell中空集对应物是Void类型，从Void到任意类型的多态函数是唯一的，称为absurd :: Void -\u0026gt; a，这是一个态射族，由于该态射存在，Void才称为类型范畴中的初始对象。\n终端对象 / Terminal Object 终端对象：有且仅有一个态射来自范畴中的任意对象。同样，终端对象在同构意义下具备唯一性。\n偏序集内，若存在终端对象，那么它是最大的那个对象。集合范畴中，终端对象是个单例，即()，有且仅有一个纯函数，从任意类型到unit类型unit :: a -\u0026gt; () （注意，唯一性的条件十分重要。因为有些集合，实际是除了空集的所有集合会有来自其它集合的态射，例如存在函数yes :: a -\u0026gt; Bool yes _ = True，但是Bool并不是终端对象，因为还有 no :: a -\u0026gt; Bool no _ = False。）\n对偶 / Duality 对于任意范畴C，反转态射方向、重新定义态射复合方式即可定义一个相反的范畴C\u0026rsquo;。这个对偶范畴自然能满足所有的范畴性质。原始态射f :: a -\u0026gt; b g :: b -\u0026gt; c用h = g . f复合得到h :: a -\u0026gt; c，那么在对偶范畴中 f' :: b -\u0026gt; a g' :: c -\u0026gt; b用h' = f' . g'复合得到h' :: c -\u0026gt; a。恒等态射保持不变。\n一个范畴中的初始对象，在另一个范畴中就是终端对象。\n泛构造 / Universal Construction 范畴论中一个常见的构造，称为泛构造，是通过对象之间的关系来定义对象。\n给出一个模式(由对象与态射构成的一种特殊的形状)，然后在范畴中观察它的各个方面。如果这个模式很常见且范畴也很大，将会有大量的符合这个模式的东西。然后对这些符合模式的东西进行排序，并选出最好的那个。一般来说，给出排序规则的时候需要一定技巧。\n下以积/Product和余积/Coproduct对泛构造的思想进行演示。\n积 / Product 已知两个集合的笛卡尔积的概念，这个笛卡尔积是个元组/序对的集合。基于此，尝试提取出“积集合”与“成分集合”之间存在的连接模式，这个连接模式便可推广到其它范畴。\n积存在两个投影p和q可将积投影成单个成分。借助此，尝试定义集合范畴中对象和态射的模式，这种模式可以引导构造两个集合a与b的积。这个模式由对象c和两个态射p::c-\u0026gt;a及q::c-\u0026gt;b构成。所有的符合这种模式的c都可以看作一个候选积，这样的c会有很多。\n第二步涉及到泛构造的另一方面：排序，或者说对比。需要有这么一个规则，能取对比符合给出模式的两个实例，最终找出最好的那个。现有两个候选，c和p::c-\u0026gt;a及q::c-\u0026gt;b、c'和p'::c'-\u0026gt;a及q'::c'-\u0026gt;b，若存在一个态射m::c'-\u0026gt;c，且由此m能重新构造p'=p.m和q'=q.m，由此可确定c比c'“更好”。另一个看待这些等式的视角是：m看作一个因子，它能将p'和q'进行“因式分解”。下图给出更直观的复合关系\n投影p和q在Haskell/F#中对应物为fst和snd，其作用是解构二元组。前者取第一个成分、后者取第二个成分。\n例如，从类型中选择Int和Bool，取它们的积。将Int和(Int, Int, Bool)作为它们的候选积。一个Int就可作为Int与Bool的积，因为它具有p x = x和q _ = True，这样它就符合了候选积的条件；单个Bool同理。三元组(Int, Int, Bool)也是一个合理的候选积，因为具备p (x, _, _) = x和q (_, _, b) = b。能够注意到，第一个候选积太小了，它只覆盖了Int方面，第二个候选积又太大了，包含了一个无用的Int维。\n现在演示(Int, Bool)二元组及其映射fst和snd为何比前面给出的候选积更好。参考下图，对于Int，存在一个m::Int-\u0026gt;(Int,Bool)例m x = (x,True)，那么伴随Int的p q可以这样构造p x = fst (m x) = x及q x = snd (m x) = True；同理，伴随三元组的m可以这样实现m (x,_,b) = (x,b)。\n已经证明了(Int, Bool)为何优于另外两者，但仅这样似乎不够完全说服人，因此此处再给出为何反过来就不成立。对于Int，是否能找到一个m'用于从p q构造出fst snd？假如存在，那么就有fst = p . m' snd = q . m'，已知伴随Int的q永远是True，而(Int, Bool)元组中第二成分是False的二元组确实存在，因此永远无法从q构造出snd。而对于(Int, Int, Bool)情况就不一样了，总是可以得到足够的信息，但是这里却存在无数个m'可以去对fst snd进行“因式分解”，例如m' (x, b) = (x, x, b) m' (x, b) = (x, 42, b)等无数个实现。\n综上，给出任意类型c和两个投影p及q，有唯一的从c到笛卡尔积(a,b)的“因子”m，可以对p q进行“因式分解”。事实上，这个m只是把p和q的结果放到了元组 m::c-\u0026gt;(a,b)、m x = (p x, q x)。这使得笛卡尔积(a,b)成为了“最好”的符合“积模式”的候选。此外，这也意味着泛构造在集合范畴上运行效果良好，它也指出了任意两个集合的积是什么。\n现在，使用相同的泛构造来定义任意范畴中两对象的积。这个积不保证一定存在，但若存在，它就在同构意义下是唯一的，且这个同构是唯一的。\nA product of two objects a and b is the object c equipped with two projections such that for any other object c’ equipped with two projections there is a unique morphism m from c’ to c that factorizes those projections.\n对象a和b的积是伴随这两个投影的对象c，对任意其它伴随两个投影的对象c'，存在唯一的态射m :: c' -\u0026gt; c，可以用这个m对c'的两个投影进行“因式分解”。\n因子生成器/factorizer是能生成因子m的高阶函数。factorizer :: (c -\u0026gt; a) -\u0026gt; (c -\u0026gt; b) -\u0026gt; (c -\u0026gt; (a, b)) 其实现 factorizer p q = \\x -\u0026gt; (p x, q x)\n余积 / Coproduct 同范畴论中每个构造一样，积的对偶是余积。将积范式中态射的方向反转，便得到了一个对象c伴随着两个入射i :: a -\u0026gt; c和j :: b -\u0026gt; c。对符合余积模式构造的排序方式也反转了，若存在一个态射m :: c -\u0026gt; c'，能对伴随c'的入射i' :: a -\u0026gt; c'及j' :: b -\u0026gt; c'进行“类因式分解”，即i' = m . i及j' = m . j，如下图所示。因此这个“最好”的对象，对任意的其它模式都有一个唯一的态射“发射”出去，这个对象就是余积。只要它存在，那么它在同构意义上就是唯一的，且这个同构也是唯一的。\nA coproduct of two objects a and b is the object c equipped with two injections such that for any other object c’ equipped with two injections there is a unique morphism m from c to c’ that factorizes those injections.\n对象a与b的余积是对象c伴随着两个入射i::a-\u0026gt;c及j::b-\u0026gt;c，对于其它任意的对象c'及伴随其的入射i'::a-\u0026gt;c'及j'::b-\u0026gt;c'，存在唯一的态射m::c-\u0026gt;c'，且该态射可以作为“因子”分解i'及j'\n在集合范畴中，余积就是两个集合的不相交求并运算。集合 a 与集合 b 的不相交求并结果中的一个元素，要么是 a 中的元素，要么是 b 中的元素。若两个集合有交集，那么余积会包含公共部分的两份拷贝。可将不相交求并运算结果的一个元素想象为贴着它所属集合的标签的元素。\n也可以为余积定义一个因式生成器。对于给定的候选余积 c 以及两个候选入射 i 与 j，为 Either生成因式函数的的因式生成器可定义为：\n1 2 3 factorizer :: (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; c) -\u0026gt; Either a b -\u0026gt; c factorizer i j (Left a) = i a factorizer i j (Right b) = j b 非对称 / Asymmetry 目前已知两种对偶结构，终端对象可由初始对象经箭头反转后获得，余积可由积经箭头反转而获得。但在集合范畴中，初始对象与终端对象的行为有显著区别，积与余积也有显著区别。积的行为像是乘法运算，终端对象扮演者 1 的角色；余积的行为更像求和运算，初始对象扮演着 0 的角色。特定情况下，对有限集，积的尺寸就是各个集合的尺寸的积；余积的尺寸是各个集合的尺寸之和。\n这一切都表明了集合的范畴不会随箭头的反转而出现对称性。\n注：空集可以向任意集合发出唯一态射（absurd函数），但它没有其他集合发来的态射。单例集合拥有任意集合发来的唯一的态射，但它也能向任意集合（除了空集）发出态射。由终端对象发出的态射在拮取其他集合中的元素方面扮演了重要的角色（空集没有元素，因此没什么东西可拮取）\n下面理解一下单例()作为积和作为余积。\n将单例()配备两个投影p与q将之作为候选积。由于积是泛构造，存在态射m :: () -\u0026gt; (a, b)，这个态射从积集合中选出一个元素（即一个元组），它也可“因式化”两个投影：p = fst . m及q = snd . m。这两个投影作用于单例值()，上面那两个方程就变为：p () = fst (m ())及q () = snd (m ())。 由于m ()是m从积集合中拮取的元素(即一个元组)，p所拮取的是参与积运算的第一个集合中的元素，结果是p ()，同理q拮取的是参与积运算的第二个集合中的元素q ()。这完全符合对积的理解，即参与积运算的集合中的元素形成积集合中的序对/元组。\n单例作为候选的余积，就不会它作为候选的积那样简单了。确实可以通过投影从单例中抽取元素，但是向单例入射就没有意义了，因为“源”在入射时会丢失。从真正的余积到作为候选余积的单例的态射也不是唯一的。集合的范畴，从初始对象的方向去看，与从终端对象的方向去看，是有显著差异的。\n这其实不是集合的性质，而是是我们在 Set 中作为态射使用的函数的性质。函数通常是非对称的，下面我来解释一下。\n函数是建立在它的定义域（Domain）上的（在编程中，称之为全函数），它不必覆盖余定义域（Codomain、陪域）。目前已经看了一些极端的例子（实际上，所有定义域是空集的函数都是极端的）：定义域是单例的函数，意味着它只在余定义域上选择了一个元素。若定义域的尺度远小于余域的尺度，通常认为这样的函数是将定义域嵌入余定义域中了。例如可以认为，定义域是单例的函数，它将单例嵌入到了余定义域中。将这样的函数称为嵌入函数，但是数学家给从相反的角度进行命名：覆盖了余定义域的函数称为满射（Surjective）函数或映成（Onto）函数。\n函数的非对称性也表现为，函数可以将定义域中的许多元素映射为余定义域上的一个元素，也就是说函数坍缩了。一个极端的例子是函数使整个集合坍缩为一个单例，unit函数就是这种行为。这种坍缩只能通过函数的复合进行混成。两个坍缩函数的复合，其坍缩能力要强过二者单兵作战。数学家为非坍缩函数取了个名字：内射（Injective）或一对一（One-to-one）映射。\n有许多函数即不是嵌入的，也不是坍缩的。它们被称为双射（Bijection）函数，它们是完全对称的，因为它们是可逆的。在集合范畴中，同构就是双射的。\n代数数据类型 / Algebraic Data Type 编程中的积类型 两个类型的积，一般实现为序对(Pair)/元组。\n序对并非严格可交换的：不能用(Int, Bool)直接替换(Bool, Int)，即便两者承载了同样的信息。在同构意义上序对是可交换的，swap :: (a, b) -\u0026gt; (b, a) swap (x, y) = (y, x)给出了同构关系，可以看作是不同的格式存储了相同的数据。\n可通过序对的嵌套完成对任意个类型组合成积，运用“嵌套的序对与元组同构”这一事实可以使构造过程更简单。例如，序对嵌套可以是(a,(b,c))或((a,b),c)，两者并不相同，但所包含的元素是一一对应的，因此这是一种同构，用不同的方式包装了相同的数据。\n可以将积类型的构造解释为作用于类型的一种二元运算。从这个角度来看，上述的同构关系看上去有些像幺半群中的结合律：(a * b) * c = a * (b * c)。只不过，在幺半群的情况中，这两种复合为积的方法是等价的，而上述积类型的构造方法只是在“同构条件下”等价。\n如果在同构条件下，不再坚持严格相等的话，能揭示unit类型()类似于数字乘法中的 1。类型 a的值与一个unit值所构成的序对，除了a值之外再无其它。因此，这种类型(a, ())与a是同构的。\n用形式化语言描述这些现象，可以说Set（集合的范畴）是一个幺半群范畴，亦即这种范畴也是一个幺半群，这意味着可以让两对象相乘（在此，就是两集合去构造笛卡尔积）。\nHaskell中有一种更通用的办法来定义积类型 date Pair a b = P a b 在此，Pair a b是由a与b参数化的类型的名字；P是数据构造子的名字。要定义一个序对类型，只需向Pair类型构造子传递两个类型。要构造一个序对类型的值，只需向数据构造子P传递两个合适类型的值。类型构造子与数据构造子的命名空间是彼此独立的，二者可以同名：data Pair a b = Pair a b。可以发现，Haskell内置的序对类型只是这种声明的一种变体，前者将数据构造子 Pair 替换为一个二元运算符 (,)。将这个二元运算符像正常的数据构造子那样用，照样能创建序对类型的值。\n如果不用泛型序对或元组，也可以定义特定的有名字的积类型，例如data Stmt = Stmt String Bool，它是 String 与 Bool 的积，有自己的名字与构造子。这种风格的类型声明，其优势在于可以为相同的内容定义不同的类型，使之在名称上具备不同的意义与功能，以避免彼此混淆。\nF#中，可以type Pair\u0026lt;'a, 'b\u0026gt; = Pair of 'a * 'b来定义序对，其本质是定义了一个单例的union。\n记录 / Record 记录是支持数据成员命名的积类型。\n例如用两个字符串表示化学元素的名称与符号，用一个整型数表示原子量，将这些信息组合到一个数据结构中。可以用元组 (String, String, Int) 来表示这个数据结构，只不过需要记住每个数据成员的含义。这样的代码很容易出错，并且也难于理解与维护。更好的办法是用记录：data Element = Element { name :: String, symbol :: String, atomicNumber :: Int }，上述的元组与记录是同构的。\nF#中，记录是这种语法type Element = {name:string; symbol:string, atomicNumber:int}\n感觉也可以这样理解，在面向对象世界里，多个字段构成一个类，就是在构造一个积的过程。\n类型可以看作集合，可以带上一些业务限制。例如班级信息包含年级、班、学生数量字段，这三者可以都是Int，但在业务意义上存在限制(例如1-6年级最多3个班最多60人)，可以看作(1~6) * (1~3) * (1~60)的积\n编程中的和类型 余积在编程中的对应物是和类型。\nHaskell官方实现的和类型是data Either a b = Left a | Right b。Either在同构的意义下是可交换的，也是可嵌套的，而且在同构意义下，嵌套的顺序不重要。因此可以定义出多个类型的和data OneOfThree a b c = Sinistral a | Medial b | Dextral c\nSet对于余积而言也是个（对称的）幺半群范畴。二元运算由不相交和（Disjoint Sum）来承担，unit元素便是初始对象。用类型术语来说，可以将Either作为幺半群运算符，将Void作为中立元素。也就是说，可以认为Either类似于运算符+，而Void类似于0。这与事实是相符的，将 Void 加到和类型上，不会对和类型有任何影响。例如：Either a Void与a同构。这是因为无法为这种类型构造Right版本的值(不存在类型为Void的值)。Either a Void只能通过Left构造子产生值，这个值只是简单的封装了一个类型为a的值。这就类似于a + 0 = a。\nHaskell中，可以将Bool实现为data Bool = True | False，就是两个单值的和。Maybe a = Nothing | Just a用于表示可能不存在，Nothing是个单值表示不存在，因此此定义可以理解成a+1，也可重定义成Maybe a = Either () a，用()表示不存在。data List a = Nil | Cons a (List a)，这是一个递归的和类型\nF#中似乎没有Either的定义，若是用的到，可以自行定义type Either\u0026lt;'a, 'b\u0026gt; = Left of 'a | Right of 'b。存在type Option\u0026lt;'a\u0026gt; = None | Some of 'a或type ValueOption\u0026lt;'a\u0026gt; = ValueNone | ValueSome of 'a。List实现类似type List\u0026lt;'a\u0026gt; = [] | :: ('a * 'a list)\n类型代数 / Algebra of Types 当前已有了类型系统中两种幺半群结构：以Void作为幺元的和类型、以()作为幺元的积类型。\n将这两种构造想象为加法和乘法，在这个视角中，Void类似于0，()类似于1。这种视角是否符合一些事实，例如：与0相乘的结果依然是0，尝试构造一个(Int, Void)序对必定失败，因为不存在一个Void值，因此(Int, Void)等价于Void，即a * 0 = 0；数学加法和乘法存在分配律a * (b + c) = a * b + a * c，对于积类型与和类型而言，在同构意义上也存在分配律，例如：(a, Either b c) = Either (a, b) (a, c)\n这种互相纠缠的幺半群称为半环/Semiring(之所以不是全环，是因为无法定义减法)。在此仅关心如何描述自然数运算与类型运算之间的对应关系。下表给出一些对应关系\nNumber Type 0 Void 1 () / unit a + b `Either a b = Left a a * b (a, b) / Pair a b 2 = 1 + 1 `Bool = True 1 + a `Maybe a = Nothing 列表类型List a = Nil | Cons a (List a)被定义为一个方程的解，因为要定义的类型出现在方程两侧，若将 List a 换成x，就可以得到这样的方程：x = 1 + a * x。\n不过，不能使用传统的代数方法去求解这个方程，因为对于类型没有相应的减法与除法运算。不过，可以用一系列的替换，即不断的用 (1 + a*x) 来替换方程右侧的 x，并使用分配律，这样就有了下面的结果：\n1 2 3 4 5 x = 1 + a*x x = 1 + a*(1 + a*x) = 1 + a + a*a*x x = 1 + a + a*a*(1 + a*x) = 1 + a + a*a + a*a*a*x ... x = 1 + a + a*a + a*a*a + a*a*a*a... 最终会是一个积（元组）的无限和，这个结果可被解释为：一个列表，要么是空的，即 1；要么是一个单例 a；要么是一个序对 a*a；要么是一个三元组 a*a*a；……以此类推，结果就是一个由 a 构成的串。\n用符号变量来解方程，这就是代数！因此上面出现的这些数据类型被称为：代数数据类型。\n注：类型a与类型b的积必须包含类型a的值与类型b的值，这意味着这两种类型都是有值的；两种类型的和则要么包含类型a的值，要么包含类型b的值，因此只要二者有一个有值即可。逻辑运算and与or也能形成半环，它们也能映射到类型理论\nLogic Types false Void true () `a a\u0026amp;\u0026amp;b (a, b) 这是更深刻的类比，也是逻辑与类型理论之间的 Curry-Howard 同构的基础\nKleisli范畴 / Kleisli Category Kleisli Categories\n\u0026lt;译\u0026gt; Kleisli 范畴\nKleisli 范畴给出了在范畴论中对副作用或非纯函数的进行构造或建模的方法。\n从编程语言的视角，可以看作是将类型（目前所见都是返回类型）进行包装（包装的主要目的是加入一些附加信息），然后对包装后的类型的处理。态射就是从任意类型a到包装类型M b的函数a -\u0026gt; M b；态射复合 (\u0026gt;=\u0026gt;) :: (a -\u0026gt; M b) -\u0026gt; (b -\u0026gt; M c) -\u0026gt; (a -\u0026gt; M c) 该运算符称为\u0026quot;fish\u0026quot;；该范畴的恒等态射为 return :: a -\u0026gt; M a，举例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 -- Writer in Haskell type Writer a = (a, String) -- 仅是在定义一个类型别名 -- 该范畴中的态射是从任意类型到Writer类型的函数 `a -\u0026gt; Writer b` -- 复合的签名如下所示 (\u0026gt;=\u0026gt;) :: (a -\u0026gt; Writer b) -\u0026gt; (b -\u0026gt; Writer c) -\u0026gt; (a -\u0026gt; Writer c) -- 复合操作符的实现 m1 \u0026gt;=\u0026gt; m2 = \\x -\u0026gt; let (y, s1) = m1 x (z, s2) = m2 y in (z, s1 ++ s2) -- 该范畴中的恒等态射 return :: a -\u0026gt; Writer a return x = (x, \u0026#34;\u0026#34;) -- ---------------------- -- 对以上进行应用 -- 定义两个操作 upCase :: String -\u0026gt; Writer String -- 将字符转换成大写，并带上附加信息 upCase = (map toUpper s, \u0026#34;upCase\u0026#34;) toWords :: String -\u0026gt; Writer [String] -- 将字符串切成多个单词，并带上附加信息 toWords = (words s, \u0026#34;toWords\u0026#34;) toUpThenToWords :: String -\u0026gt; Writer [String] -- 将以上两个操作复合起来 toUpThenToWords = upCase \u0026gt;=\u0026gt; toWords 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // Writer in F# type Writer\u0026lt;\u0026#39;a\u0026gt; = \u0026#39;a * string // 仅定义了类型别名 let (\u0026gt;=\u0026gt;) m1 m2 = fun x -\u0026gt; // 这里推断为`m1:(\u0026#39;a -\u0026gt; \u0026#39;b * int) -\u0026gt; m2:(\u0026#39;b -\u0026gt; \u0026#39;c * int) -\u0026gt; x:\u0026#39;a -\u0026gt; \u0026#39;c * int` let (y, s1) = m1 x let (z, s2) = m2 y (z, s1 + s2) let toUpper str = Writer (str.ToUpper(), \u0026#34;toUpper\u0026#34;) // 实际上没必要带上`Writer` let toWords str = Writer (str.ToWords(), \u0026#34;toWords\u0026#34;) let proc = toUpper \u0026gt;=\u0026gt; toWords // 可以将`Writer`声明为单实例的联合 type Writer\u0026lt;\u0026#39;a\u0026gt; = Writer of \u0026#39;a * string let (\u0026gt;=\u0026gt;) m1 m2 = fun x -\u0026gt; let (Writer (y, s1)) = m1 x let (Writer (z, s2)) = m2 y Writer (z, s1 + s2) 函子 / Functor 函子是范畴之间的映射，给定两个范畴C与D，函子可以将范畴C中的对象映射为D中的对象，将C中的态射映射为D中的态射并“保持结构”。若C中有一个对象a，它在D中的像即为F a；C中有一个态射f从对象a出发指向对象b，f在D中的像就是F f，它连接了a在D中的像与b在D中的像，如下图所示\n范畴结构中还包含态射复合及恒等态射。若在范畴C中有复合h = g . f，被函子F映射后，有F h = F g . F f；范畴C中对象a的恒等态射$id_a$被映射成范畴D中的$id_{F a}$，且$F id_a = id_{F a}$\n对函子的要求相对严格，因为要求函子保持范畴的结构，它可能会将一些对象“打碎”，也可能会将多个态射“合并”成一个，但它绝不会将任何东西丢弃掉，这种保持结构的约束类似于代数中的连续性条件，也就是说或函子具有“连续性”（函子的连续性还存在更多的限定概念）。\n与函数类似，函子可以做折叠或嵌入的工作。所谓嵌入，就是将一个小的源范畴嵌入到更大的目标范畴中。一个极端的例子，源范畴是个单例范畴——只有一个对象与一个态射（恒等态射）的范畴，从单例范畴映射到任何其他范畴的函子，所做的工作就是在后者中选择一个对象。这完全类似于接受单例集合的态射，这个态射会从目标集合中选择元素。最巨大的折叠函子被称为常函子$\\Delta_C$ ，它将源范畴中的每个对象映射为目标范畴中特定的对象c，也可以将源范畴中的每个态射映射为目标范畴中的特定的恒等态射，在行为上像一个黑洞，将所有东西压成一个奇点。\n自函子 / Endofunctor 自函子是映射的目标范畴与源范畴相同的函子。\n类型类 / Type Class Haskell中使用类型类对函子进行抽象，一个类型类定义了支持一个公共接口的类型族。类型类是Haskell仅有的函数/运算符重载机制。\n例如，定义支持相等谓词的类型类如下。该定义陈述的是，如果类型a支持(==)运算符，那么它就是Eq类。(==)运算符接受两个类型为a的值，返回Bool值。使用时声明某个特定类型是Eq族的一个“实例”，并提供(==)的实现，如下所示\n1 2 3 4 5 6 7 8 9 10 11 -- 定义一个类型类 class Eq a where (==) :: a -\u0026gt; a -\u0026gt; Bool -- 定义某个类型 data Point = Pt Float Float -- 声明该类型是Eq类型的实例，并给出相应实现 -- 注：在用到的`Point`的`Eq`功能时声明即可，不必在定义`Point`时声明 instance Eq Point where (Pt x y) == (Pt x\u0026#39; y\u0026#39;) = x == x\u0026#39; \u0026amp;\u0026amp; y == y\u0026#39; 函子类型类Functor定义如下：\n1 2 class Functor f where fmap :: (a -\u0026gt; b) -\u0026gt; f a -\u0026gt; f b 这个类型类规定了：若存在符合以上签名的fmap方法，那么这个f是个函子。小写的f是个类型变量，类似于类型变量a与b，然而编译器能够推断出它是类型构造子而不是类型，依据是它的用途：它可作用于其他类型，即f a与f b。因此，要声明一个Functor的实例时，必须给出一个类型构造子，此处就是Maybe：\n1 2 3 instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just x) = Just (f x) F#不支持类型类，似乎仅能用静态类型解析进行约束，很麻烦很扯淡，此外需要新语言功能release才行（静态扩展允许作为类型解析的证据？）\n编程中的函子 函子（这里仅关注编程中的自函子）不仅仅只映射对象（编程中对应物为类型），也映射态射（编程中对应物为函数）。\n任意被其他类型参数化了的类型，都可视为候选函子。可将函子视为一个抽象的容器\nMaybe函子 以Haskell中的Maybe为例，其定义data Maybe a = Nothing | Just a就是在将类型a映射为Maybe a。注意Maybe不是一个类型，是个类型构造子，需要提供一个类型参数如Int/Bool才能使之变成类型Maybe Int/Maybe Bool。对于任意函数f :: a -\u0026gt; b，Maybe函子需将之映射成f' :: Maybe a -\u0026gt; Maybe b，f'的实现为f' Nothing = Nothing及f' (Just x) = Just (f x)，即函数的参数是Nothing，那么返回Nothing即可；若这个函数的参数是Just，就将f应用于Just的内容。一般以高阶函数的形式来实现函子的态射映射部分，一般称fmap，例如对于Maybe函子，其fmap :: (a -\u0026gt; b) -\u0026gt; (Maybe a -\u0026gt; Maybe b)。通常说fmao“提升”/lift了一个函数，被提升的函数可以作用于Maybe层次上的值。由于Currying存在，对于fmap的签名(a -\u0026gt; b) -\u0026gt; Maybe a -\u0026gt; Maybe b有两个看待视角，一是接受一个a -\u0026gt; b类型的函数值，返回一个Maybe a -\u0026gt; Maybe b类型的函数值；另一个是接受一个a -\u0026gt; b类型的函数值和一个Maybe a类型的值，返回一个Maybe b类型的值。\n为了证明类型构造子 Maybe 携同函数 fmap 共同形成一个函子，必须证明 fmap 能够维持恒等态射以及态射的复合。所证明的东西，叫做“函子定律”。凡是满足函子定律的函子，必定不会破坏范畴的结构。\nF#及Scala中的对应物是Option，此外，F#中还有ValueOption\nList函子 1 2 3 4 5 data List a = Nil | Cons a (List a) instance Functor List where fmap _ Nil = Nil fmap f (Cons x t) = Cons (f x) (fmap f t) List是类型构造子，将任意类型a映射为类型List a。为了证实List是一个函子，必须定义一个“提升”函数fmap :: (a -\u0026gt; b) -\u0026gt; (List a -\u0026gt; List b)，它接受一个a -\u0026gt; b函数，产生一个List a -\u0026gt; List b函数。\nReader 函子 Haskell中使用箭头类型构造子(-\u0026gt;)构造函数类型。形如a -\u0026gt; b是对其的中缀使用形式，也可写作前缀形式(-\u0026gt;) a b。该符合的偏应用/部分应用是合法的，例如(-\u0026gt;) a是一个接受一个类型参数的类型构造子，它需要一个类型 b 来产生完整的类型 a -\u0026gt; b，它所表示的是，它定义了一族由a参数化的类型构造子。\n可以将参数类型称为r，将返回类型称为a。因此类型构造子可以接受任意类型a，并将其映射为类型r -\u0026gt; a。为了证实这是个函子，需要一个提升函数fmap :: (a -\u0026gt; b) -\u0026gt; (r -\u0026gt; a) -\u0026gt; (r -\u0026gt; b)，它可以将函数a -\u0026gt; b“提升”为从r -\u0026gt; a到r -\u0026gt; b的函数，而r -\u0026gt; a与r -\u0026gt; b就是(-\u0026gt;) r 这个类型构造子分别作用于a与b所产生的函数类型。对于给定的函数f :: a -\u0026gt; b与g :: r -\u0026gt; a，构造一个函数r -\u0026gt; b。复合两个函数是唯一途径，也恰恰就是当前所需。因此，fmap的实现为fmap f g = f . g，或者直接写作fmap = (.)\n综上，类型构造子(-\u0026gt;) r与这个fmap组合形成的函子便是Reader函子\n1 2 instance Functor ((-\u0026gt;) r) where fmap = (.) Writer 函子 在Kleisli范畴中，态射是被“装饰”后的函数，返回的是一个type Writer a = (a, String)数据类型。这种“装饰”跟自函子有些关系，实际上Writer类型构造子对于a具备函子性。无需为之实现fmap，它只是个简单的积类型。\nKleisli范畴，定义了复合与恒等。复合是通过小鱼运算符实现的，恒等态射是一个叫做return的函数\n1 2 3 4 5 6 7 8 9 10 -- composition (\u0026gt;=\u0026gt;) :: (a -\u0026gt; Writer b) -\u0026gt; (b -\u0026gt; Writer c) -\u0026gt; (a -\u0026gt; Writer c) m1 \u0026gt;=\u0026gt; m2 = \\x -\u0026gt; let (y, s1) = m1 x (z, s2) = m2 y in (z, s1 ++ s2) -- identity return :: a -\u0026gt; Writer a return x = (x, \u0026#34;\u0026#34;) 基于以上两个函数，可以直接给出fmap :: (a -\u0026gt; b) -\u0026gt; (Writer a -\u0026gt; Writer b)的实现：fmap f = id \u0026gt;=\u0026gt; (\\x -\u0026gt; return (f x))，这个fmap证实了Writer是个函子。注意实现里面小鱼运算符左侧id :: Writer a -\u0026gt; Writer a，这里(\u0026gt;=\u0026gt;) :: (a -\u0026gt; Writer b) -\u0026gt; (b -\u0026gt; Writer c) -\u0026gt; (a -\u0026gt; Writer c)并未约束a必须是个常规类型，它可以是任意类型，当然可以是个Writer b。因此fmap中的小鱼运算符接受了Writer a -\u0026gt; Writer a和a -\u0026gt; Writer b并最终返回了Writer a -\u0026gt; Writer b。\n注意，以上是可以推广的：可以将Writer替换为任何一个类型构造子。只要这个类型构造子支持一个小鱼运算符以及 return，那就可以定义fmap。因此Kleisli范畴中的这种“装帧”，实际上是一个函子。（尽管并非每个函子都能产生一个 Kleisli 范畴）\n这样定义的fmap否与编译器使用deriving Functor自动继承来的fmap是相同的。这是Haskell实现多态函数的方式所决定的。这种多态函数的实现方式叫做参数化多态，它是所谓的免费定理（Theorems for free）之源。这些免费的定理中有一个是这么说的，如果一个给定的类型构造子具有一个fmap的实现，它能维持恒等（将一个范畴中的恒等态射映射为另一个范畴中的恒等态射），那么它必定具备唯一性。\n函子作为容器 Haskell模糊了数据与代码的区别。可以将列表视为函数，也可以将函数视为从存储着参数与结果的表中查询数据。如果函数的定义域有界并且不太大，将函数变成表查询是完全可行的。将函子对象（由自函子产生的类型的实例）视为包含着一个值或多个值的容器，即使这些值实际上并未出场；或者说函子对象可能包含产生这些值的方法，不必关心能否访问这些值——这些事发生在函子作用范围之外。如果函子对象包含的值能够被访问，就可以看到相应的操作结果；若不能被访问，那么所关心的只是操作的正确复合，以及伴随不改变任何事物的恒等函数的相关操作。\n1 2 3 4 data Const c a = Const c instance Functor (Const c) where fmap _ (Const v) = Const v 例如data Const c a = Const c，Const类型构造子接受两种类型， c与a，但它忽略参数a。类似之前处理箭头构造子那样，对其进行偏应用从而制造了一个函子。数据构造子（也叫Const）仅接受c类型的值，它不依赖a。与这种类型构造子相配的fmap类型为：fmap :: (a -\u0026gt; b) -\u0026gt; Const c a -\u0026gt; Const c b。因为这个函子是忽略类型参数的，所以fmap的实现可以忽略函数参数\n函子的复合 函子的复合类似于集合之间的函数复合。两个函子的复合，就是两个函子分别对各自的对象进行映射的复合，对于态射也是这样。恒等态射穿过两个函子之后，它还是恒等态射。复合的态射穿过两个函子之后还是复合的态射。函子的复合只涉及这些东西。自函子很容易复合。\n以MaybeTail :: [a] -\u0026gt; Maybe [a]为例，其实现MaybeTail [] = Nothing MaybeTail [x::xs] = Just xs，返回的结果是两个作用于a的函子Maybe与[]复合后的类型。这两个函子每个都有自己的fmap，若想将函数f作用于被Maybe []包含的内容，需要突破两层函子的封装。例如若要对mis :: Maybe [Int]中包含的数字mis = Just [1, 2, 3]用square x = x * x求平方，可以这样做：mis2 = fmap (fmap square) mis。经类型分析过后，对于内部的fmap使用List版本，外部的fmap使用Maybe版本。它可重写为mis2 = (fmap . fmap) square mis，重写版本是fmap :: (a -\u0026gt; b) -\u0026gt; (f a -\u0026gt; f b)视角，(fmap . fmap)中第二个fmap接受square :: Int -\u0026gt; Int返回[Int] -\u0026gt; [Int]，第一个fmap接受这个函数并返回Maybe [Int] -\u0026gt; Maybe [Int]，最终作用于mis并求得结果。两个函子的复合结果依然是函子，并且这个函子的 fmap是那两个函子对应的fmap的复合\n函子的复合是遵守结合律的，因为对象的映射遵守结合律，态射的映射也遵守结合律。在每个范畴中也有一个恒等函子：它将每个对象都映射为其自身，将每个态射映射为其自身。因此在某个范畴中，函子具有与态射相同的性质。什么范畴会是这个样子？必须得有一个范畴，它包含的对象是范畴，它包含的态射是函子，也就是范畴的范畴。但是，所有范畴的范畴还必须包含它自身，这样我们就陷入了自相矛盾的境地，就像不可能存在集合的集合那样。但是，存在一个叫做 Cat 的范畴，它包含了所有的“小范畴”。这个范畴是一个“大的范畴”，因此它就不可能是它自身的成员。所谓的“小范畴”，就是它包含的对象可以形成一个集合，而不是某种比集合还大的东西。注意，在范畴论中，即使一个无限的不可数的集合也被认为是『小』的。以后也会看到函子也能形成范畴。\n二元函子 / Bifunctor 函子是Cat范畴（范畴的范畴）中的态射，因此对于态射形成的直觉（编程中的函数）大部分可适用于函子。直觉上能理解一个函数接受两个参数，函子也可。接受两个参数的函子称为二元函子。对于对象而言，若一个对象来自范畴$C$，另一个对象来自范畴$D$，那么二元函子可以将这两个对象映射为范畴$E$中的某个对象。也就是说，二元函子是将范畴$C$与范畴$D$的笛卡尔积 $C \\times D$映射为$E$，如下图所示。函子性也意味着二元函子也可以映射态射，也就是二元函子必须将一对态射（即范畴$C \\times D$中的一个态射，其中一个来自$C$，一个来自$D$）映射为$E$中的一个态射。\n注，若一个态射是在范畴的笛卡尔积中定义的，那么它的行为就是将一对对象映射为另一对对象。这样的态射可以复合(f, g) ∘ (f', g') = (f ∘ f', g ∘ g')，这样的复合是符合结合律的。此外，也有恒等态射(id, id)。范畴的笛卡尔积也是一个范畴。\n将二元函子想象为具有两个参数的函数会更直观一些。要证明二元函子是否是函子，不必借助函子定律，只需独立的考察它的参数即可。如果有一个映射，它将两个范畴映射为第三个范畴，只需证明这个映射相对于每个参数（例如，让另一个参数变成常量）具有函子性，那么这个映射就自然是一个二元函子。对于态射，也可以这样来证明二元函子具有函子性。\n个人理解：“二元函子”作为使“范畴的范畴”构成一个幺半群所需的二元运算。这需要联系幺半群定义去理解。\n1 2 3 4 5 6 7 class Bifunctor f where bimap :: (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; d) -\u0026gt; f a b -\u0026gt; f c d bimap g h = first g . second h -- or: second h . first g first :: (a -\u0026gt; c) -\u0026gt; f a x -\u0026gt; f c x first g = bimap g id second :: (b -\u0026gt; d) -\u0026gt; f x b -\u0026gt; f x d second = bimap id 以上是用Haskell定义了一个二元函子，本例中，三个范畴都是Haskell类型范畴。一个二元函子是个类型构造子，它接受两个类型参数，类型变量f表示二元函子，关于它的所有应用都是在接受两个类型参数。二元函子伴随着一个“提升”函数bimap，它将两个函数映射为(f a b -\u0026gt; f c d)函数。bimap有一个默认的实现，即first与second的复合，这表明只要bimap分别对两个参数都具备函子性，就意味着它是一个二元函子(如下图)。\n其他两个类型签名是first与second，他们分别作用于bimap的第一个与第二个参数，因此它们是f具有函子性的两个fmap证据(如下图)。上述类型类的定义以bimap的形式提供了first与second的默认实现。\n当声明Bifunctor的一个实例时，可以去实现bimap，这样first与second就不用再实现了；也可以去实现first与 second，这样就不用再实现bimap。当然也可以三个都实现了，但是需要确定它们之间要满足类型类的定义中的那些关系。\n积于余积二元函子 / Product and Coproduct Bifunctors 由泛构造定义的两个对象的积，即范畴积，是二元函子的一个重要例子。若任意两个对象之间存在积，那么从这些对象到积的映射便具备二元函子性。这通常是正确的，尤其是在Haskell中。\n1 2 instance Bifunctor (,) where bimap f g (x, y) = (f x, g y) 序对构造子作为最简单的积类型，就是一个Bifunctor的实例。bimap :: (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; d) -\u0026gt; (a, b) -\u0026gt; (c, d)作用是很清晰的。这个二元函子的用途就是产生的类型序对(,) a b = (a, b)\n1 2 3 instance Bifunctor Either where bimap f _ (Left x) = Left (f x) bimap _ g (Right y) = Right (g y) 余积作为对偶，若它是作用于范畴内的每一对对象，那么它也是二元函子。以上给出了Haskell中余积二元函子Either的例子。\n具备函子性的代数数据类型 / Functorial Algebraic Data Types 代数数据类型/ADT具备函子性。\n目前所见的几个参数化数据类型的例子都是函子（可以为之定义fmap）。复杂的数据类型是由简单数据类型构造出来的，尤其是ADT，它是由和与积构造而来。目前以已确定积与和具备函子性及函子的复合，因此只需证实ADT的基本构造块具备函子性，那么就可以确定ADT基本函子性。\n首先，有些构造块是不依赖于函子所接受的类型参数的，例如Maybe中的Nothing或者List中的Nil。它们等价与Const函子（Const函子忽略它的类型参数\u0026mdash;实际上是忽略第二个类型参数，第一个被保留作为常量）。其次，有些构造块简单的将类型参数封装为自身的一部分，例如Maybe中的Just，它们等价于恒等函子。恒等函子是Cat范畴中的恒等态射，Haskell未对它进行定义，这里给出：\n1 2 3 data Identity a = Identity a instance Functor Identity where fmap f (Identity x) = Identity (f x) 可将Identity视为最简单的容器，它只存储类型a的一个（不变）的值。其他的代数数据结构都是使用这两种基本类型的和与积构建而成。\n基于此来看待Maybe，data Maybe = Nothing | Just a。这是两种类型的和，求和是具备函子性的。第一部分Nothing可以表示为作用于类型a的Const ()（Const的第一个类型参数是unit），而第二部分不过是恒等函子的化名而已。在同构的意义下可以将Maybe定义为：type Maybe a = Either (Const () a) (Identity a)。Maybe是Const ()函子与Identity函子被二元函子Either复合后的结果。(Const本身也是一个二元函子，只不过在这里用的是它的偏应用形式)\n两个函子的复合后，其结果是一个函子。此外，还需要确定两个函子被一个二元函子复合后如何作用于态射。对于给定的两个态射，可以分别用这两个函子对其进行提升，然后再用二元函子去提升这两个被提升后的态射所构成的序对。\n可以在Haskell中定义这种复合 newtype BiComp bf fu gu a b = BiComp (bf (fu a) (gu b))，定义二元函子bf、两个函子fu与gu、两个常规类型a与b，将a应用到fu，b应用到gu，然后将fu a与gu b应用到bf。这是对象的复合，在Haskell中也是类型的复合。将Either、Const ()、Identity，a和b应用到BiComp得到的BiComp (Either (Const () a) (Identity b))，这就是一个裸奔版本的Maybe。\nbf是二元函子、fu与gu是函子，那么这个新的数据类型BiComp就是a与b的二元函子。编译器必须知道与 bf 匹配的 bimap 的定义，以及分别与 fu 与 gu 匹配的 fmap 的定义。在 Haskell 中，这个条件可以预先给出：一个类约束集合后面尾随一个粗箭头：\n1 2 3 instance (Bifunctor bf, Functor fu, Functor gu) =\u0026gt; Bifunctor (BiComp bf fu gu) where bimap f1 f2 (BiComp x) = BiComp ((bimap (fmap f1) (fmap f2)) x) 伴随BiComp的bimap实现是以伴随bf的bimap以及两个分别伴随fu和gu的fmap给出的。在使用bimap时，编译器会自动推断出所有类型，并选择正确的重载函数。bimap的定义中，x 的类型为bf (fu a) (gu b)，有点复杂。外围的bimap脱去它的bf层，然后两个fmap分别脱去它的fu与gu层。若f1 :: a -\u0026gt; a'及f2 :: b -\u0026gt; b'，那么，最终结果是类型 bf (fu a') (gu b')，因此bimap签名可以理解为bimap :: (fu a -\u0026gt; fu a') -\u0026gt; (gu b -\u0026gt; gu b') -\u0026gt; bf (fu a) (gu b) -\u0026gt; bf (fu a') (gu b')，这其实与bimap :: (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; d) -\u0026gt; f a c -\u0026gt; f b d并无区别。\n因此没有必要去证明Maybe是个函子，它是两个基本的函子求和后的结果，因此Maybe自然具备函子性。\n对于代数数据类型而言，Functor实例的继承相当繁琐，这个过程可以由编译器自动完成。\n1 2 3 4 {-# LANGUAGE DeriveFunctor #-} -- 在代码首部启用Haskell扩展 data Maybe a = Nothing | Just a deriving Functor -- 声明该数据结构是个函子，然后就会得到相应的`fmap`的实现。 代数数据结构的规律性不仅适用于Functor的自动继承，也适合其它的类型类，例如之前提到的Eq类型类。也可以要求Haskell编译器自动继承自定义的类型类，但是技术上要难一点。不过思想是相同的：为类型类描述基本构造块、求和以及求积的行为，然后让编译器来描述其他部分。\n协变与逆变函子 / Covariant and Contravariant Functor 回顾Reader函子，Reader函子(-\u0026gt;) r是“函数箭头”类型构造子的的偏应用（箭头-\u0026gt;本身就是一个类型构造子，它接受两个类型参数）。为之取一个类型别名，并将之声明为Functor实例\n1 2 3 4 type Reader r a = r -\u0026gt; a instance Functor (Reader r) where fmap f g = f . g 函数类型构造子接受两个类型参数，这一点与序对或Either类型构造子相似。序对与Either对于它们所接受的参数都具备函子性，因此它们二元函子。函数类型构造子是否是个二元函子？如果是，那么它必须对于两个类型参数都具备函子性，以上定义只给出了(-\u0026gt;)对于它的第二个类型参数具备函子性，现在尝试证明对于第一个参数具备函子性。\n需要固定第二个参数，使第一个参数可变，因此type Op r a = a -\u0026gt; r。此时对于Op r，返回类型r固定了下来，只让参数类型是a可变的。与它相匹配的fmap的类型签名如下：fmap :: (a -\u0026gt; b) -\u0026gt; (a -\u0026gt; r) -\u0026gt; (b -\u0026gt; r)。从fmap的类型签名看出，只凭借(a -\u0026gt; b)和(a -\u0026gt; r)类型的参数，无法构造出(b -\u0026gt; r)。但是如果存在某种方式能够反转a -\u0026gt; b，使之变成b -\u0026gt; a，那么目标构造便能成立。虽然不能随便反转一个函数的参数，但是在对偶范畴中可以这样做。\n对于每个范畴$C$都存在一个对偶范畴$C^{OP}$，后者包含的对象与前者相同，后者包含的态射与前者一致但方向相反。假设范畴$C^{OP}$与另一个范畴$D$之间存在一个函子$F :: C^{OP} \\rightarrow D$，这个函子将$C^{OP}$中的一个态射$f^{OP} :: a \\rightarrow b$映射为$D$中的一个态射$F f^{OP} :: F a \\rightarrow F b$。该态射$f^{OP}$与范畴$C$中的态射$f :: b \\rightarrow a$相对应，两者方向是相反的。其形状如下图所示：\n现在$F$是一个常规的函子，基于此定义一个映射$G$，该映射不是熟悉的普通函子。这个映射从范畴$C$到范畴$D$，映射对象时功能与$F$相同，映射态射时会先将态射方向反转，然后再使用$F$的功能。$G$接受$C$中的一个态射$f :: b \\rightarrow a$，将其映射为相反的态射$f^{OP} :: a \\rightarrow b$，然后将函子$F$作用于这个被反转的态射$f^{OP}$，最终得到$F f^{OP} :: F a \\rightarrow F b$。$F a$与$G a$相同，$F b$与$G b$相同，因此$G f :: \\lparen b \\rightarrow a \\rparen \\rightarrow \\lparen G a \\rightarrow G b \\rparen$。这种反转了态射方向的映射便称为“逆变函子”/Contravariant Functor。注意逆变函子只是来自对偶范畴的一个“常规函子”。之前所述的Maybe及List等都是“常规函子”，它们称为“协变函子”/Covariant Funcotr。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- Haskell中逆变函子类型类的定义 （实际是逆变自函子） class Contravariant f where contramap :: (b -\u0026gt; a) -\u0026gt; f a -\u0026gt; f b -- `Op`是它的一个实例 instance Contravariant (Op r) where -- contramap :: (b -\u0026gt; a) -\u0026gt; (a -\u0026gt; r) -\u0026gt; (b -\u0026gt; r) contramap f g = g . f -- 函数 `f` 插入到了 `g` 之前 -- contramap 只是个颠倒了参数顺序的复合运算符 -- flip 函数可以用于颠倒参数顺序 flip :: (a -\u0026gt; b -\u0026gt; c) -\u0026gt; (b -\u0026gt; a -\u0026gt; c) flip f y x = f x y -- 基于此 contramap = flip (.) 副函子 / Profunctor 函数箭头运算符对于它的第一个参数具有逆变函子性，对于第二个参数具有协变函子性，在集合范畴Set中，这被称为副函子/Profunctor。由于一个逆变函子相当于其对偶范畴中的协变函子，因此可以这样定义一个副函子：$C^{OP} \\times D \\rightarrow Set$。\n由于Haskell的类型系统可看作集合范畴，因此可以在其中定义副函子类型类。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- 从 Data.Profunctor 库中抽取出来的类型类 class Profunctor p where dimap :: (b -\u0026gt; a) -\u0026gt; (c -\u0026gt; d) -\u0026gt; p a c -\u0026gt; p b d dimap f g = lmap f . rmap g lmap :: (b -\u0026gt; a) -\u0026gt; p a x -\u0026gt; p b x lmap f = dimap f id rmap :: (c -\u0026gt; d) -\u0026gt; p x c -\u0026gt; p x d rmap = dimap id -- 三个函数只是默认的实现。类似Bifunctor，当声明Profunctor实例时，要么实现dimap，要么实现lmap和rmap -- 现在声明函数箭头运算符是Profunctor的实例 instance Profunctor (-\u0026gt;) where dimap :: (b -\u0026gt; a) -\u0026gt; (c -\u0026gt; d) -\u0026gt; (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; d) dimap ba cd ac = cd . ac . ba lmap = flip (.) rmap = (.) 函数类型 / Function Type 构造函数类型 尝试运用泛构造的知识构造一个函数类型，或者从广义上说，构造一个内hom-集。\n可将函数类型视为一种复合类型，因为它描述的是一个参数类型和结果类型之间的关系。积类型和余积类型便是复合类型的泛构造，这里运用同样的技巧。\n首先给出对象及联系对象的模式。这里有三个对象：要构造的函数类型、参数类型和返回类型。显然，联系这三者的模式就是函数应用或求值。对于函数类型，假设存在一个候选者z(在非集合范畴中，z只是个普通对象)，设参数类型为a(一个对象)，函数应用可将z与a构成的序对映射为结果类型b(一个对象)。现在可以认为已有三个对象了，其中两个已固定(参数类型和返回类型)，另一个是“应用”。“应用”是一种映射，若能查看对象内部，那么可以将一个函数f(z的一个元素)与参数x(a的一个元素)封装为序对，然后将这个序对映射为f x(f作用于x，结果是b的一个元素）只处理单个的序对(f, x)并无太大价值，要处理的是函数类型的候选者z与参数类型a的积，即z × a。这个积是一个对象，可以选择从这个对象到b的一个箭头g作为态射，g也就是“应用”。在集合的范畴中，g就是将每个(f, x)映射为f x的函数。这样就建立起了这样一个模式：对象z与对象a的积，通过态射g被关联到另一个对象b。\n要利用泛构造技巧来清晰的刻画函数类型，这样的模式够用么？这个模式不适于所有范畴，但是对于我们感兴趣的那些范畴，它够用了。还有一个问题：定义一个函数类型，必须事先定义积类型吗？有些范畴是不可积的，或者并非所有成对的对象都能构成积。这个问题的答案是不行，如果没有积类型，就没有函数类型。\n以上构造通常会命中很多东西，它们都符合这个模式。特别是在集合的范畴中，几乎每一样东西都与其他东西具有相关性。可以选取任意一个对象z，将其与a形成积，再找个函数将积映射为b（除非b是空集）。因此需要建立排序机制并找出最好的那个。当且仅当存在一个唯一的从z'到z的映射，使得g'可由g来构造，即可判定伴随态射g(从z×a到b)的z比伴随g'的候选者z'更好。如下图所示\n假设存在态射h:: z' -\u0026gt; z，想获得从z' × a到z × a的态射。由于积类型具备函子性，或者说积类型本身是个函子(更确切的说，是二元自函子)，因此可以提升一对态射。也就是说不仅能定义对象的积，也能定义态射的积。不需要改变z' × a这个积的第二个成员，因此要提升的态射序对是(h, id)，其中id是作用于a的恒等态射。现在有g' = g ∘ (h × id)。\n泛构造的第三个步就是选出最好的那个候选者，将之称为a⇒b(在这里，将它视为一个对象的名字即可，不要与 Haskell 类型类的约束符号混淆，下文会给出其他命名方式)。这个对象伴随的“应用”称为eval，即eval :: (a⇒b) × a -\u0026gt; b。若其他候选者所对应的“应用”g都能由eval唯一的构造出来，那么a⇒b就是最好的那个候选者。\n其形式定义如下：\n一个从a到b的函数对象是伴随着态射eval :: ((a⇒b) × a) -\u0026gt; b的a⇒b，它对于伴随着态射g :: z × a -\u0026gt; b的任意其它对象z而言，存在唯一的态射h :: z -\u0026gt; (a⇒b)，使得g = eval ∘ (h × id)。\n不能确保对于某个范畴中的任意的对象a与b都存在着a⇒b，但是对于集合的范畴却总是存在着这样的a⇒b，并且在集合的范畴中a⇒b与Hom-集**Set(a,b)**同构。这也是在Haskell中将函数类型a -\u0026gt; b解释为范畴意义上的函数对象a⇒b的原因。\n柯里化 / Curring “接受两个参数的函数”与“接受一个参数并返回函数的函数”存在一一对应的关系，这种对应关系叫做柯里化\n再次观察函数类型的所有候选者，这次将态射g视为接受两个参数的函数g :: z x a -\u0026gt; b(一个接受积类型的态射与一个接受两个变量的函数很相似，尤其是在集合范畴中，g是接受值的序对的函数，其中一个值来自集合z、另一个来自集合a)；另一方面，基于泛性质，可以知道对于每个这样的g都存在唯一的态射h能将z映射为函数类型a⇒b，即h :: z -\u0026gt; (a⇒b)。在集合范畴中，这意味着h是一个接受z类型参数并返回一个从a到b的函数的函数，h是一个高阶函数。这种泛构造在“接受两个参数的函数”与“接受一个参数并返回函数的函数”建立的一一对应的关系，这种对应关系叫做柯里化，且h称为g的柯里化版本。\n这种关系是一一对应的。对于任意函数g都存在唯一的h，而对于任意的h也总是能重加一个接受两个参数的函数g，且g = eval ∘ (h × id)。可以将g称h的反柯里化版本。\n柯里化是Haskell和F#内置的语法，返回一个函数的函数a -\u0026gt; (b -\u0026gt; c)可直接视为一个接受两个参数的函数a -\u0026gt; b -\u0026gt; c，这样能直接支持函数的部分应用(即先只给出一个参数，产生一个接受单一参数的函数)。Scala不直接支持这一概念，需要显式定义多参数列表的方法def f[A,B,C](a: A)(b: B): C = ???，这种函数才能支持部分应用\n严格将，接受两个参数的函数，其本质接受的是一个序对/积类型，即(a, b) -\u0026gt; c，这也一般是函数式编程语言与面向对象编程语言交互时看待“对象方法”的方式。但是两种形式的转换一般很容易\n1 2 3 4 5 6 -- Haskell似乎内置了一下两个函数 curry :: ((a, b)-\u0026gt;c) -\u0026gt; (a-\u0026gt;b-\u0026gt;c) curry f a b = f (a, b) uncurry :: (a-\u0026gt;b-\u0026gt;c) -\u0026gt; ((a, b)-\u0026gt;c) uncurry f (a, b) = f a b 1 2 3 // F# 中似乎没有内置，但是也很容易就能实现出来 let curry f a b = f (a, b) let uncurry f (a, b) = f a b 指数 / Exponential 在数学领域，从对象$a$到对象$b$的函数或内hom-对象(内hom-集中的对象)通常称为指数，表示为$b^a$。注意，函数参数类型位于指数的位置。\n上文构造函数类型时提到了必须借助积才行，然而函数与积还有更深层的联系。考虑建立在有限类型(即值的数量为有限的类型，例如Bool、Char甚至Int、Double等)上的函数，理论上，这种函数是“可记忆”的，可将这些函数运算转换成查表操作，这也是函数(态射)与函数类型(对象)之间等价的本质。例如，一个接受Bool的纯函数可以被特化为一对值，一个对应于True另一个对应于False。那么，所有从Int到Bool的函数等价于所有Int序对构成的集合，用积来表示就是Int x Int，或者$Int^2$，即$Int^{Bool}$\n实践中并不会要求一个接受Int或Double的函数做成查表实现，因为这样不切实际。但应当承认函数与数据类型之间的等价性确实存在。Haskell是个惰性求值的语言，在惰性求值的（无限的）数据结构与函数之间的界限并不那么明显。这种函数与数据之间的对偶性揭示了Haskell的函数类型与范畴化的指数对象之间的等价性。\n指数与代数数据类型 从指数的角度来阐释函数类型，这种方式也能很好的适用于代数数据类型。事实上，中学代数中所涉及的0、1、加法、乘法以及指数等结构，在任何双向笛卡尔闭范畴中同样存在，它们分别对应于初始对象、终端对象、余积、积以及指数等。现在还没有足够的工具（诸如伴随（Adjunction）或 Yoneda 定理）来证明这一点，不过在此可以直观呈现。\n0次幂 $a^0 = 1$ 在范畴论中，0即初始对象，1即终端对象，“相等”即恒等态射，指数即内hom-对象。这个特殊的指数表示的是从初始对象到任意对象$a$的态射集合。基于初始对象的定义，这样的态射只有一个，因此hom-集$C(0,a)$是一个单例集合。一个单例集合在集合范畴中是终端对象，因此上面这个等式在集合范畴中是成立的，这也意味着它在任何双向笛卡尔闭范畴中都成立。\nHaskell中用Void表示0，用unit类型()表示1，用函数类型表示指数。所有从Void到任意类型a的函数集合等价于unit类型，即单例集合。换句话说，有且仅有一个函数Void -\u0026gt; a，这个函数之前提到过，叫做absurd。注意，这样解释存在一些漏洞，原因有二。首先，Haskell中不存在没有值的类型，每种类型都包含着“永不休止的运算”，即底；此外，absurd的所有实现都是等价的，无论如何实现，都不可能对其进行调用，因为没有值可以传递给absurd（如果传递给它一个永不休止的运算，它永远也不会返回结果）\n1的幂 $1^a = 1$ 在集合范畴中，这个等式重申了终端对象的定义：从任意对象到终端对象存在唯一的态射。从a到终端对象的内hom-对象通常与终端对象本身是同构的。\n在Haskell中，只有一个函数是从任意类型a到unit类型的，这个函数叫unit，也可以认为它是const函数对()的偏应用。\n1次幂 $a^1 = a$ 这个等式重申了从终端对象出发的态射可用于从对象a中拮取元素。这种态射的集合与对象a本身是同构的。在集合范畴与 Haskell中，集合a与从a中拮取元素的函数() -\u0026gt; a是同构的。\n指数的和 $a^{b + c} = a^b \\times a^c$ 从范畴论的角度来看，这个等式描述的幂为两个对象的余积的指数与两个指数的积同构。\n在Haskell中这种代数等式的解释是：两个类型的和的函数与两个参数类型为单一类型的函数的积同构。这恰恰就是在定义作用于和类型的函数时所用到的分支分析，也就是说，函数定义中的case语句可以用两个或多个处理特定类型的函数来替代。例如，下面这个从和类型(Eigher Int Double)出发的函数f :: Either Int Double -\u0026gt; String 可以定义为一个函数对：\n1 2 3 f (Left n) = if n \u0026lt; 0 then \u0026#34;Negative int\u0026#34; else \u0026#34;Positive int\u0026#34; f (Right x) = if x \u0026lt; 0.0 then \u0026#34;Negative double\u0026#34; else \u0026#34;Positive double\u0026#34; -- 在此，`n` 是 `Int`，而 `x` 是 `Double`。 指数的指数 $(a^b)^c = a^{b \\times c}$ 这个等式表达的是指数对象形式的柯里化，即返回一个函数的函数等价与积类型的函数（带两个参数的函数）。\n积的指数 $(a \\times b)^c = a^c \\times b^c$ 在Haskell中，返回一个序对的函数与一对函数等价，后者的每个函数都返回序对的一个元素。\n笛卡尔闭范畴 / Cartesian Closed Category 包含终端对象、任意对象序对的积以及任意对象序对的指数的范畴是笛卡尔闭范畴。集合范畴就属于此类范畴。\n将指数看作重复的积（可能是无限次），那就可以将笛卡尔闭范畴视为支持任意数量的积运算的范畴。特别地，可将终端对象视为0个对象的积，或者一个对象的0次幂。从计算机科学的角度来看，笛卡尔闭范畴为简单的类型Lambda演算（类型化的编程语言的基础）提供了模型。\n终端对象与积也分别具有对偶物：初始对象与余积。笛卡尔闭范畴也支持这两者，积通过分配率可转化为余积：a × (b + c) = a × b + a × c (b + c) × a = b × a + c × a。这样的范畴被称为双向笛卡尔闭范畴。\n柯里-霍华德同构 / Curry-Howard Isomorphism 逻辑学与代数数据类型之间有一些对应关系。Void类型与unit类型()分别对应于错误与正确；积类型与和类型分别对应于逻辑与运算$\\vee$与逻辑或运算$\\wedge$。遵循这一模式，函数类型对应于逻辑推理$\\Longrightarrow$，换句话说，类型a -\u0026gt; b可以读为“如果 a 那么 b”。\n根据柯里-霍华德同构理论，每种类型皆可视为一个命题，它们是为真或为假的陈述语句。如果类型是有值的，那么它就是真命题，否则就是伪命题。在实践中，如果一个函数类型有值，亦即存在这样的函数，那么与它对应的逻辑推理就为真。去实现一个函数，就是在证明一个定理。写程序，就等价于证明许多定理。\n以函数类型定义中所用的eval函数为例，它的签名是：eval :: ((a -\u0026gt; b), a) -\u0026gt; b。它接受一个由函数与其参数构成的序对，产生相应的类型。这个函数是一个态射的Haskell实现，该态射为：eval :: (a⇒b) × a -\u0026gt; b。这个态射定义了函数类型a⇒b（或指数类型$b^a$）。运用柯里-霍华德同构理论，可将这个签名转化为逻辑命题：$((a \\Longrightarrow b) \\vee a ) \\Longrightarrow b$ 可将上面这条陈述读为：如果b由a推出为真，且a为真，那么b肯定为真。这就是所谓的肯定前件式。要证明这个定理，只需要实现一个函数，即eval :: ((a -\u0026gt; b), a) -\u0026gt; b eval (f, x) = f x。如果有一个从a到b的函数f以及a类型的一个值x所构成的序对，就可以将f作用于x，从而产生b类型的一个值。通过实现这个函数，可以证明((a -\u0026gt; b), a) -\u0026gt; b是有值的。因此，在该逻辑中，这一肯定前件式为真。\n再给出一个结果为假的逻辑命题。看这个例子，如果a或b为真，那么a肯定为真：$a \\wedge b \\Longrightarrow b$。这个命题肯定是错的，因为当a为假而b为真时，就可以构成一个反例。运用柯里-霍华德同构理论，可将这个命题映射为函数签名：Either a b -\u0026gt; a，根本无法实现这样的函数，因为对于Right构造的值而言，无法产生类型为a的值（注：仅限纯函数）\n最后，重新理解absurd :: Void -\u0026gt; a。将Void视为假，可得：$false \\Longrightarrow a$，这意味着由谎言可推理出一切（爆炸原理）。对于这个命题（函数），下面用 Haskell 给出的一个证据（实现）：absurd (Void a) = absurd a 其中Void的定义如下：newtype Void = Void Void 这是惯用的花招，这个定义使得Void不可能用于构造一个值，因为要用它构造一个值，前提是必须先提供这个类型的一个值，这样就使得absurd永远无法被调用\n自然变换 / Natural Transformation 自然变换是保持函子性质不变的特殊映射，它是函子之间的映射。\n函子可以在维持范畴结构的前提下实现范畴之间的映射。函子可以将一个范畴嵌入到另一个范畴，也可以让多个范畴坍缩为一个范畴且不会破坏范畴的结构，甚至可以在一个范畴之内构建另一个范畴。源范畴可视为目标范畴的部分结构的模型或蓝图。将一个范畴嵌入到另一个范畴可能有许多种方式，这些方式有时是等价的，有时不等价。可以将整个的范畴坍缩为另一个范畴中的一个对象，也可以将一个范畴中的每个对象映射为另一个范畴中不同的对象，并将前者中的每个态射映射为后者中的不同的态射。同样的想法可以有多种不同方式的实现。自然变换可以用于对比这些实现。\n对于范畴$C$与$D$之间的两个函子$F$与$G$，若只关注$C$中的一个对象$a$，它被映射为$D$中的两个对象：$F a$和$G a$，那么应该存在一个函子映射$\\alpha_a$，它可以将$F a$映射为$G a$，如下图所示\n由于在同一范畴中的对象映射应该不会脱离该范畴，因此不想再额外建立$F a$和$G a$的联系，因此很自然地考虑使用现有态射。自然变换本质上是如何选取态射：对于任意对象$a$，自然变换就是选取一个从$F a$到$G a$的态射。若将一个自然变换称为$\\alpha$，那么这个态射就称为在$a$上的$\\alpha$分量(Component of $\\alpha$ at $a$)，记作$\\alpha_a$。注：$\\alpha_a :: F a \\rightarrow G a$，此外$a$是一个在$C$中的对象，而$\\alpha_a$是$D$中的一个态射。对于某个$a$，若在$F a$与$G a$之间没有态射，那么$F$与$G$之间也就不存在自然变换。\n函子映射的不只是对象，它也能映射态射，而自然变换对态射的映射是固定的，在两函子$F$与$G$之间的任意自然变换下，$F f$必须映射到$G f$。如下图所示，范畴$C$中两个对象$a$与$b$之间的态射$f$，被映射为范畴$D$中的两个态射$F f :: F a \\rightarrow F b$和$G f :: G a \\rightarrow G b$。自然变换$\\alpha$提供了两个态射(即为$\\alpha$在$a$及$b$上的两个分量，$\\alpha_a :: F a \\rightarrow G a$和$\\alpha_b :: F b \\rightarrow G b$)补全了$D$中的结构。为保证两个从$F a$到$G b$的途径是等价的，必须要引入自然性条件/Naturality Condition：$G f \\circ \\alpha_a = \\alpha_b \\circ F f$\n自然性条件对于任意态射$f$都成立。若态射$F f$是可逆的，基于自然性条件能给出$\\alpha_b = G f \\circ \\alpha_a \\circ (F f)^{-1}$(即$\\alpha_a$表示的$\\alpha_b$)，如下图所示。若两个对象之间存在多个可逆的态射，上述变换也都成立。尽管态射通常是不可逆的，两个函子之间的自然变换也并非一定存在。与自然变换相关的函子的多寡，可在很大程度上显现这些函子所操纵的范畴的结构。\n从分量的角度看待自然变换，可以认为自然变换$\\alpha$将范畴$C$中的对象$a$映射为范畴$D$中的态射$\\alpha_a$；从自然性角度来看，可认为自然变换将态射$f$映射为一个正方形交换图，如下所示。自然变换的这一性质可以让很多范畴便于构造，这些范畴往往包含着这类的交换图。在正确选择函子的情况下，大量的交换条件都能够转换为自然性条件。\n自然变换可用于定义函子的同构。若自然变换的各个分量都是同构的（态射可逆），那么这两个函子自然同构。若两个函子是自然同构，差不多是在说它们是相同的函子。\n多态函数 / Polymorphic Function 函子（确切地说是自函子）在编程中有广泛应用，它能将类型映射为类型（其自身具备类型构造子功能），也能将函数映射为函数（借助高阶函数fmap实现）\n假定存在函子F可将类型a映射为F a和函子G可将类型a映射为G a，在a上的自然变换的alpha分量是一个从F a到G a的函数alpha_a :: F a -\u0026gt; G a。自然变换alpha是面向所有类型a的多态函数alpha :: forall a . F a -\u0026gt; G a（forall a在Haskell中是可选功能，可以用语言扩展ExplicitForAll开启，通常可将其写为alpha :: F a -\u0026gt; G a），这是一个由a参数化的函数族。C++中与之类似的构造有些复杂template\u0026lt;Class A\u0026gt; G\u0026lt;A\u0026gt; alpha(F\u0026lt;A\u0026gt;);\nHaskell的多态函数与C++的泛型函数之间存在很大的区别，主要体现为函数的实现方式以及类型检查方式上。Haskell中，一个多态函数必须对于所有类型是唯一的，一个公式必须适用于所有的类型，这是所谓的参数化多态/Parametric polymorphism；C++默认提供的是特设多态/Ad hoc polymorphism，这意味着模板不一定涵盖所有类型，一份模板是否适用于某种给定的类型需要在实例化时方能确定，彼时编译器会用一种具体的类型来替换模板的类型参数，类型检测是以推导的形式实现的，因此编译器经常会给出难以理解的错误信息。在C++中，还有一种函数重载与模板特化机制，通过这种机制可以为不同的类型定义函数的不同版本。Haskell有类似的机制，即类型类（Type class）与类型族（Type family）\nHaskell中这种类型的多态函数alpha :: F a -\u0026gt; G a，函子F与G自动满足自然性条件（即$G f \\circ \\alpha_a = \\alpha_b \\circ F f$）。Haskell中函子G作用于一个态射f是通过fmap实现的，可表示为fmap_G f . alphaa = alphab . fmap_F f，Haskell的类型推导十分强大，类型标记是不需要的，因此可写为fmap f . alpha = alpha . fmap f。这不是真正的Haskell代码（在代码中无法表示函数等式），但它给出了恒等，在等式推导中可以使用这个公式，编译器也可以利用这个公式对代码进行优化。\n之所以在Haskell里自动满足自然性条件，这是“免费定理”的自然结果。Haskell里，将参数化多态用于定义自然变换，会引入非常强的限制条件：一个公式必须适应所有类型。这些限制条件会变成面向这些函数的方程一样的定理。对于能够对函子进行变换的函数，免费的定理是自然性条件。\n在Haskell中，函子可以视为泛型容器，继续这个类比，自然变换只是一个重组方式，将一个容器中的东西取出来放到零一个容器里，自然变换不触碰这些东西。于是，自然性条件就体现为：这个重组方式不关心我们是先通过fmap修改这些东西，然后再将它们放到新容器里，还是先放到新容器里再用适用于新容器的fmap去修改它们，重组与fmap是正交的。\n下面给出几个例子\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 -- `List`与`Maybe`函子之间的自然变换`safeHead` safeHead :: [a] -\u0026gt; Maybe a safeHead [] = Nothing safeHead (x:xs) = Just x -- 它需要满足的自然性条件： fmap f . safeHead = safeHead . fmap f -- 用`fmap`对其进行验证 fmap :: (a -\u0026gt; b) -\u0026gt; f a -\u0026gt; f b -- `List`函子的`fmap` fmap f [] = [] fmap f (x:xs) = f x : fmap f xs -- `Maybe`函子的`fmap` fmap f Nothing = Nothing fmap f (Just x) = Just (f x) -- 空列表 fmap f . (safeHead []) = fmap f Nothing = Nothing safeHead (fmap f []) = safeHead [] = Nothing -- 非空列表 fmap f (safeHead (x:xs)) = fmap f (Just x) = Just (f x) safeHead (fmap f (x:xs)) = safeHead (f x : fmap f xs) = Just (f x) 以上是个常见的自然变换的样子。但一个以Const函子为始点或终点的自然变换，看上去就像一个函数，它既面向它的返回类型多态，也面向它的参数类型多态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- `length`视为从列表函子到`Const Int`函子的自然变换： length :: [a] -\u0026gt; Const Int a length [] = Const 0 length (x:xs) = Const (1 + unConst (length xs)) -- `unConst`用于剥除`Const`构造子 unConst :: Const c a -\u0026gt; c unConst (Const x) = x -- 实际上`length`的定义： length :: [a] -\u0026gt; Int -- 这个定义有效地掩盖了`length`作为自然变换的本质 ------------------------------------- -- 寻找一个以`Const`函子为始点的参数化多态函数有点难，因为这需要无中生有创造一个值出来。能想到的最好办法是： scam :: Const Int a -\u0026gt; Maybe a scam (Const x) = Nothing 还有一个不同寻常的函子，它在Yoneda引理中扮演了重要的角色，这个函子就是Reader函子\n1 2 3 4 -- 这个函子被两种类型参数化，仅第二个类型具备协变函子性： newtype Reader e a = Reader (e -\u0026gt; a) instance Functor (Reader e) where fmap f (Reader g) = Reader (\\x -\u0026gt; f (g x)) 对于每种类型e，都可以定义从Reader e到任何其他函子f的自然变换族，这个家族的成员总是与f e的元素一一对应（Yoneda引理）。考虑仅有一个值()的类型unit，函子Reader ()接受任意类型a，然后形成函数类型() -\u0026gt; a，这些函数可以从集合a中提取一个元素，函数的数量与a中的元素一样多。考虑该函子到Maybe函子的自然变换alpha :: Reader () a -\u0026gt; Maybe a，这样的自然变换只有dumb（dumb (Reader _) = Nothing）和obivous（obvoius = (Reader g) = Just (g ())）。实际上按照 Yoneda 引理的说法，这与Maybe ()类型的两个元素相符，即 Nothing 与 Just ()（注意该说法并不严谨）\n超自然性 / Beyond Naturality 两个函子之间的参数化多态函数（包括Const函子这种边界情况）必定是自然变换。因为所有的标准代数数据类型都是函子，在这些类型之间的任何一个多态函数都是自然变换。函数类型的返回类型具备函子性，可以运用这一特点来构造函子（例如Reader函子），并为这些函子构造自然变换，这些自然变换是更高阶的函数。\n但是，函数类型的参数类型具备的是逆变函子性（逆变函子就是对偶范畴中的协变函子）。在范畴意义上，两个逆变函子之间的多态函数依然可视为自然变换，当然它们只能作用于Haskell类型范畴的对偶范畴里的函子。\n1 2 3 4 5 6 7 8 9 10 11 12 -- 逆变函子`Op`对于`a`具有逆变性 newtype Op r a = Op (a -\u0026gt; r) instance Contravariant (Op r) where -- contramap :: (b -\u0026gt; a) -\u0026gt; (Op r a -\u0026gt; Op r b) -- contramap :: (b -\u0026gt; a) -\u0026gt; (a -\u0026gt; r) -\u0026gt; (b -\u0026gt; r) contramap f (Op g) = Op (g . f) -- 一个从`Op Bool`到`Op String`的函数 predToStr (Op f) = Op (\\x -\u0026gt; if f x then \u0026#34;T\u0026#34; else \u0026#34;F\u0026#34;) -- 函子`Op Bool`和`Op String`不具备协变性，它们不是**Hask**范畴中的自然变换。但它们具备逆变性，因此满足“相反的”自然性条件： contramap f . predToStr = predToStr . contramap f -- 注：函数`f`必须走与`fmap`作用下方向相反的方向 存在不是函子的类型构造子，例如a -\u0026gt; a。类型参数a出现在负（逆变）位与正（协变）位上，对于这种类型，fmap或contramap都无法实现。符合函数签名(a -\u0026gt; a) -\u0026gt; f a（其中f是任意函子）的函数不是自然变换。存在着一种广义的自然变换，叫作双自然变换，它们能够处理这些情况。\n函子范畴 / Functor Category 对于每对范畴$C$和范畴$D$，存在且仅存在一个函子范畴，在这个范畴里，对象是从$C$到$D$的函子，态射是函子间的自然变换。对于每个函子$F$，存在一个恒等自然变换$1_F$，它的分量恒等态射$id_{F a} :: F a -\u003e F a$；已知自然变换的分量是态射，自然变换的复合就是各分量态射的复合，态射复合遵循结合律，因此自然变换复合也遵循结合律。\n例如，现有函子$F$到函子$G$的自然变换$\\alpha$和函子$G$到函子$H$的自然变换$\\beta$，它们在对象$a$上的分量是某个态射$\\alpha_a :: F a \\rightarrow G a$和$\\beta_a :: G a \\rightarrow H a$，这两个态射是可以复合的，复合结果是另一个态射$\\beta_a \\circ \\alpha_a :: F a \\rightarrow H a$，可以将这个结果作为自然变换$\\beta \\cdot \\alpha$（自然变换$\\alpha$和$\\beta$的复合，复合顺序先$\\alpha$后$\\beta$）在$a$上的分量，即$(\\beta \\cdot \\alpha)_a = \\beta_a \\circ \\alpha_a$(如下图所示)\n复合后的自然变换依然满足自然性条件$H f \\circ (\\beta \\cdot \\alpha)_a = (\\beta \\cdot \\alpha)_b \\circ H f$，如下图所示\n关于自然变换复合的记法\n以上的给出的示意图里面，函子是从上向下堆砌的，这种称为竖向复合，用小圆点$\\cdot$来表示。此外还有横向复合，用小圆圈$\\circ$表示，有时可能是星号。\n范畴$C$与范畴$D$之间的函子范畴记作$Func(C, D)$或$[C, D]$，有时也记作$D^C$。最后这种记法暗示了可将函子范畴本身视为一个其它范畴中的函数对象（指数）\n范畴由对象和态射构成；范畴（严格来讲是小范畴，它们的对象形成集合）本身是更高层范畴Cat中的对象，Cat中的态射是函子；Cat里的Hom-集是函子构成的集合。例如$Cat(C,D)$是范畴$C$与$D$之间的函子集合，函子范畴$[C, D]$也是两个范畴间的函子集合（自然变换作为态射），$[C, D]$中的对象也就是$Cat(C,D)$集合中的东西。此外，函子范畴自身也是范畴，它也是Cat中的对象之一（也就是说，两个小范畴之间的函子范畴本身也很小）。一个范畴里的Hom-集与同一个范畴里的对象之间存在联系（即内hom-集），这种情况就类似之前看到的指数形式的对象\n为构造一个指数，首先要定义积。在Cat里，定义积相当容易。小范畴是对象的集合，集合之上可以定义笛卡尔积，积范畴$C \\times D$中的一个对象，是两对象构成的序对$(c, d)$（一个来自范畴$C$，一个来自范畴$D$）；类似地，$(c, d)$与$(c', d')$之间的态射是一个态射序对$(f, g)$，其中$f :: c \\rightarrow c'$、$g :: d \\rightarrow d'$；态射序对由来自两范畴的态射构成，总会有一个分别由两范畴的恒等态射构成的序对。Cat是一个完全的笛卡尔闭范畴，对于任意一对范畴$C$和$D$，它里面存在着相应的指数对象$D^C$。Cat里的对象是范畴的，因此$D^C$是范畴，它就是范畴$C$与$D$之间的函子范畴\n2-Categories / 2-范畴 2-范畴是一个广义的范畴，其中具备对象和态射（准确讲，应该是1-态射），还有2-态射（即态射之间的态射）。\n按照定义，Cat里任意Hom-集都是函子集合，其中两对象之间的函子形成一个函子范畴，函子范畴中以自然变换作为态射。在Cat里，对象是（小）范畴，函子是对象间的态射，自然变换就是态射间的态射。这个更“丰满”的结构便是2-范畴的一个例子。\n用函子范畴$D^C$的形式来代替范畴$C$和$D$之间的Hom-集。现有常规的函子符合：来自$D^C$的函子$F$与来自$E^D$的函子$G$复合，可以得到来自$E^C$的函子$G \\circ F$。此外，在每个Hom-范畴内部，也存在着复合，这就是函子之间的自然变换或2-态射的竖向复合，如下图所示。\n要分析清楚这两种复合之间关系，首先从Cat里面选两个函子（或者说1-态射）$F :: C \\rightarrow D$和$G :: D \\rightarrow E$，以及它们的复合$G \\circ F :: C \\rightarrow E$，此外还有两个自然变换$\\alpha :: F \\rightarrow F'$和$\\beta :: G \\rightarrow G'$，整体关系如下图。显然，不可能对两个自然变换应用竖向复合，因为$\\alpha$的终点与$\\beta$起点不重合（实际上因为它们属于两个函子范畴$D^C$和$E^D$）。\n基于以上条件，很明显可以复合函子$F'$和$G'$得到$G' \\circ F'$。现在尝试基于已有的$\\alpha$和$\\beta$定义一个从$G \\circ F$到$G' \\circ F'$的自然变换。首先给出简图如下\n从范畴$C$中的一个对象$a$开始，它分裂为$D$中的两个对象$F' a$和$F' a$，此外有一个态射$\\alpha_a :: F a \\rightarrow F' a$连接这两个对象，这个态射是$\\alpha$的分量；在从$D$到$E$的时候，两个对象分裂成四个对象$G (F a)$、$G' (F a)$、$G (F' a)$和$G' (F' a)$，还有四个态射形成了一个方格，其中有两个是$\\beta$的分量：$\\beta_{F a} :: G (F a) \\rightarrow G' (F a)$和$\\beta_{F' a} :: G (F' a) \\rightarrow G' (F' a)$，另两个是$\\alpha_a$在两个函子下的象（函子提升了的态射）：$G \\alpha_a :: G (F a) \\rightarrow G (F' a)$和$G' \\alpha_a :: G' (F a) \\rightarrow G' (F' a)$。目标是从中找出$G (F a)$到$G' (F' a)$的态射，找到了$G' \\alpha_a \\circ \\beta_{F a}$和$\\beta_{F' a} \\circ G \\alpha_a$，这两者是相等的。这四个态射形成的方格对于$\\beta$而言具备自然性。将这个自然变换称为$\\alpha$与$\\beta$的横向复合：$\\beta \\circ \\alpha :: G \\circ F \\rightarrow G' \\circ F'$\n复合的存在依赖于范畴。自然变换的纵向符合存在于函子范畴，现找出横向复合存在的范畴。解决这个问题需要从侧面来看Cat，不要将自然变换看成函子之间的态射，而是将它们看成范畴之间的态射。一个自然变换位于两个范畴之间，而这两个范畴原本是由这个自然变换所变换的函子连接的，可以认为这个自然变换连接着这两个范畴。从Cat里面选择两个对象，即范畴$C$和$D$，两对象之间存在着由自然变换构成的集合，这些自然变换来往于连接$C$和$D$的函子之间，将这些自然变换视为从$C$到$D$的态射。同理，也有一些自然变换是来自于连接范畴$D$和$E$的函子，将它们视为从$D$到$E$的态射。于是，横向复合就是这种态射的复合。存在从范畴$C$到范畴$C$的恒等态射，它是范畴$C$上恒等函子自身的恒等自然变换。注：横向复合的恒等也是竖向复合的恒等，但反过来不是。\n横向复合和纵向复合满足交换律：$(\\beta' \\cdot \\alpha') \\circ (\\beta \\cdot \\alpha) = (\\beta' \\circ \\beta) \\cdot (\\alpha' \\circ \\alpha)$。\n此后可能会看到更多记法。在从侧面看待Cat的视角里，从一个对象到另一个对象有两种办法：使用函子或自然变换。此外，可以将函子态射解读为一种特殊的自然变换：恒等自然变换作用于这个函子。基于这个解读，$F \\circ \\alpha$便是合理的，其中$F$是从$D$到$E$的函子、$\\alpha$是从$C$到$D$的自然变换，函子和自然变换当然无法复合，这个记法解读为恒等自然变换$1_F$与$\\alpha$的横向复合（复合顺序先$\\alpha$后$1_F$）。\nMonad / 单子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 -- Kleisli范畴中的恒等为`return :: a -\u0026gt; m a`、复合为`\u0026gt;=\u0026gt; :: (a -\u0026gt; m b) -\u0026gt; (b -\u0026gt; m c) -\u0026gt; (a -\u0026gt; m c)` -- 尝试实现这一复合 (\u0026gt;=\u0026gt;) :: (a -\u0026gt; m b) -\u0026gt; (b -\u0026gt; m c) -\u0026gt; (a -\u0026gt; m c) (\u0026gt;=\u0026gt;) f g = \\a -\u0026gt; let mb = f a -- 拿到一个`a`，也只能将之应用到`f`，得到`mb` in mb ??? g -- 需要`mb ??? g`，从而得到`mc`，`???`需要具备把“函子容器”打开的能力 --- ???处一般称作bind，用`\u0026gt;\u0026gt;=`表示，其定义 (\u0026gt;\u0026gt;=) :: m a -\u0026gt; (a -\u0026gt; m b) -\u0026gt; m b -- 因此，一个实现了bind和return的即为一个单子m class Monod m where (\u0026gt;\u0026gt;=) :: m a -\u0026gt; (a -\u0026gt; m b) -\u0026gt; m b return :: a -\u0026gt; m a -- 单子m是函子，可以借助函子已有的工具来定义`\u0026gt;\u0026gt;=` ma \u0026gt;\u0026gt;= f = ??? (fmap f ma) -- `fmap f ma`得到了`m (m b)`类型的值，需要有个更基本的工具将“两层函子容器”解除一层，这一工具是`join` join :: m (m a) -\u0026gt; m a -- 因此，简单扩展一下函子，添加几个相当基本的函数，即可得到一个单子 class Functor m =\u0026gt; Monad m where join :: m (m a) -\u0026gt; m a return :: a -\u0026gt; m a 以上几个概念在范畴论中的表示如下所示\nCode Math m $T$ join join :: m (m a) -\u0026gt; m a $\\mu$ $\\mu :: T^2 \\rightarrow T$ return return :: a -\u0026gt; m a $\\eta$ $\\eta :: Id \\rightarrow T$ 在范畴论中，自函子$T$和两个自然变换$\\mu$和$\\eta$能够成一个单子，此外要求自然变换遵守结合律和恒等法则。\nmonad is a monoid in the endofunctor category.\nMonad (category theory)\nMonad (functional programming)\n看了下维基上的相关内容，还有很多概念尚不理解，留待之后更新\nWhat is the difference between monoid and monad?\nTO BE FIXED\nMonad Update ","date":"2020-04-07T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/ctfp-notes-1/","title":"范畴论笔记 - 第一部分"},{"content":"Natural Transformations - 自然变换 Natural Transformations\n\u0026lt;译\u0026gt; 自然变换\n函子可以在维持范畴结构的前提下实现范畴之间的映射。函子可以将一个范畴嵌入到另一个范畴，它也可以让多个范畴坍缩为一个范畴且不会破坏范畴的结构。将一个范畴嵌入到另一个范畴可能有许多种方式。有时这些方式是等价的，有时它们不等价。可以将整个的范畴坍缩为另一个范畴中的一个对象，也可以将一个范畴中的每个对象映射为另一个范畴中不同的对象，将前者中的每个态射映射为后者中的不同的态射。自然变换可以对比这些实现。自然变换是函子之间的映射——可以保持函子性质不变的特殊映射。\n对于范畴 C 与 D 之间的两个函子 F 与 G，如果我们只关注 C 中的一个对象 a，它被映射为 D 中的两个对象：F a 与 G a，那么应该存在一个函子映射，它可以将 F a 映射为 G a。由于在同一范畴中的对象映射应该不会脱离该范畴，而且我们不想人工建立 F a 与 G a 的联系，因此很自然的考虑使用既有的联系——所谓的态射。自然变换本质上是如何选取态射：对于任意对象 α ，自然变换就是选取一个从 F a 到 G a 的态射。如果将一个自然变换称为 α，那么这个态射就被称为在 a 上的 α 分量（Component of α at a），记作 $\\alpha_a$ α_a: α_a :: F a -\u0026gt; G a\n记住，a 是一个在 C 中的对象，而 α_a 是 D 中的一个态射。 对于某个 a，如果在 F a 与 G a 之间没有态射，那么 F 与 G 之间也就不存在自然变换。\n函子所映射的不止是对象，它们也能映射态射，自然变换应该如何对待这种映射？答案是，态射的映射是固定的——在 F 与 G 之间的任何一个自然变换下，F f 必须变换成 G f。也就是说，两个函子对态射所形成的映射会彻底限制与之相适的自然变换的定义。来考虑在范畴 C 中的两个对象 a 与 b 之间的态射 f，它被映射为范畴 D 中的两个态射 F f 与 G f：\n1 2 F f :: F a -\u0026gt; F b G f :: G a -\u0026gt; G b 自然变换 α 提供两个附加的态射来补全 D 中的结构：\n1 2 α_a :: F a -\u0026gt; G a α_b :: F b -\u0026gt; G b 现在，便有了两个从 F a 到 G b 的途径。为了保证这两种途径等价，必须引入自然性条件（Naturality condition）：G f ∘ α_a = α_b ∘ F f 这个条件应该对于任意的 f 都成立。\n自然性条件非常有用。例如，如果态射 F f 是可逆的，自然性决定了以 α_a 形式表示的 α_b。通过 f，基于自然性条件可将 α_a 变换为：α_b = (G f) ∘ α_a ∘ (F f)^(-1)\nPolymorphic Functions - 多态函数 之前讲过函子（更确切的说，是自函子）在编程中所扮演的角色。它们相当于类型构造子——将类型映射为类型。不过，函子也能将函数映射为函数，这种映射是通过一个高阶函数 fmap（在 C++ 中则是 transform, then 之类的行为）。\n为了构造一个自然变换，我们从一个对象开始，也就是一种类型，设为 a。一个函子 F，可以将 a 映射为类型 F a。另一个函子 G，可以将 a 映射为 G a。在 a 上的自然变换 alpha 的分量是一个从 F a 到 G a 的函数。使用用伪 Haskell 代码，可将其表示为：alpha_a :: F a -\u0026gt; G a 通常，可将其写为：alpha :: F a -\u0026gt; G a 这是由 a 参数化的一个函数族。这是 Haskell 语法简洁性的又一个示例。在 C++ 中，与之类似的构造要麻烦一些： template\u0026lt;Class A\u0026gt; G\u0026lt;A\u0026gt; alpha(F\u0026lt;A\u0026gt;);\nHaskell 的多态函数与 C++ 的泛型函数之间存在很大的区别，主要体现为函数的实现方式以及类型检查方式上。在 Haskell 中，一个多态函数必须对于所有类型是唯一的。一个公式必须适用于所有的类型。这就是所谓的参数化多态（Parametric polymorphism）。C++ 默认提供的是特设多态（Ad hoc polymorphism），这意味着模板不一定涵盖所有类型。一份模板是否适用于某种给定的类型需要在实例化时方能确定，彼时，编译器会用一种具体的类型来替换模板的类型参数。类型检测是以推导的形式实现的，编译器经常会给出难以理解的错误信息。在 C++ 中，还有一种函数重载与模板特化机制，通过这种机制可以为不同的类型定义函数的不同版本。Haskell 也有类似的机制，即类型类（Type class）与类型族（Type family）。\nHaskell 的参数化多态有一个一个不可预料的结果。凡是像这种类型的多态函数：alpha :: F a -\u0026gt; G a 函子 F 与 G 自动满足自然性条件。这里，我们再回到范畴论的概念（f 是一个函数，f::a-\u0026gt;b）：G f ∘ α_a = α_b ∘ F f 在 Haskell 中，函子 G 作用于一个态射 f 是通过 fmap 实现的。可使用 Haskell 伪码将上述概念表示为：fmap_G f . alphaa = alphab . fmap_F f 归功于 Haskell 的类型推导，上述类型标记是不需要的，因此可写为以下形式：fmap f . alpha = alpha . fmap f 这依然不是真正的 Haskell 代码——在代码中无法表示函数等式——不过，上式是恒等的，程序员在等式推导中可以使用这个公式，此外，编译器也可以利用这个公式对代码进行优化。\n自然性条件之所以在 Haskell 里会自动被满足，这是『免费的定理』的自然结果。在 Haskell 里，参数化多态，将其用于定义自然变换，会引入非常强的限制条件——一个公式适应所有类型。这些限制条件会变成面向这些函数的方程一样的定理。对于能够对函子进行变换的函数，免费的定理是自然性条件。\n在 Haskell 里，可以将函子视为泛型容器。我们可以继续这个类比，将自然变换视为一种重组方法，即将一个容器里的东西取出来放到另一个容器里。我们不会触碰这些东西：不修改，也不创造。我们只是将它们（或它们的一部分）复制到新的容器里，在这个过程中有时候会对它们作几次乘法。于是，自然性条件就是，首先它不关心我们是先通过 fmap 修改这些东西，然后再将它们放到新容器里，还是先把它们放到新容器里再用适用于这个容器的 fmap 去修改它们。重组与 fmap，它们是正交的\n来看一下 Haskell 里的自然变换。首先来看列表与 Maybe 这两种函子之间的自然变换，它的功能是，当且仅当列表非空时返回列表的首元素：\n1 2 3 safeHead :: [a] -\u0026gt; Maybe a safeHead [] = Nothing safeHead (x:xs) = Just x 它是面向 a 的函数多态化。它可以不受限制地作用于任意一种类型 a，因此它是一个参数化多态的例子。从而，它就是两个函子之间的一个自然变换。不过，现在这只是我们在自以为是，下面来验证它是否符合自然性条件。fmap f . safeHead = safeHead . fmap f\n1 2 3 4 5 6 7 -- 考虑两种情况；一个空列表： fmap f (safeHead []) = fmap f Nothing = Nothing safeHead (fmap f []) = safeHead [] = Nothing -- 和一个非空列表： fmap f (safeHead (x:xs)) = fmap f (Just x) = Just (f x) safeHead (fmap f (x:xs)) = safeHead (f x : fmap f xs) = Just (f x) 当函子之一是 Const 函子的时候，会发生一件有趣的事。一个以 Const 函子为始点或终点的自然变换，看上去就像一个函数，它即面向它的返回类型多态，也面向它的参数类型多态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 -- 可将 length 视为从列表函子到 Const Int 函子的自然变换： length :: [a] -\u0026gt; Const Int a length [] = Const 0 length (x:xs) = Const (1 + unConst (length xs)) -- unConst 用于剥除 Const 构造子： unConst :: Const c a -\u0026gt; c unConst (Const x) = x -- 实际上 length 的定义是下面这样： length :: [a] -\u0026gt; Int -- 这个定义有效地掩盖了 length 作为自然变换的本质。 寻找一个以 Const 函子为始点的参数化多态函数有点难，因为这需要无中生有创造一个值出来。我们所能想到的最好的办法是：\n1 2 scam :: Const Int a -\u0026gt; Maybe a scam (Const x) = Nothing 还有一个不同寻常的函子，在 Yoneda 引理中扮演了重要的角色。这个函子就是 Reader 函子。下面我用 newtype 来重写一下它的定义：newtype Reader e a = Reader (e -\u0026gt; a) 这个函子被两种类型参数化了，但是它的（协变）函子性仅着落在第二个类型上：instance Functor (Reader e) where fmap f (Reader g) = Reader (\\x -\u0026gt; f (g x)) 对于每种类型 e，都可以定义从 Reader e 到任何其他函子的自然变换家族。以后会看到，这个家族的成员总是与 f e 的元素壹壹对应（Yoneda 引理）。\n例如，考虑有时会被忽略的仅有一个值 () 的 unit 类型 ()。函子 Reader () 接受任何一种类型 a，将它射入函数类型 () -\u0026gt; a。这些函数可以从集合 a 中拮取一个元素。这些函数的数量与 a 中元素的数量一样多。现在，来看一下从这种函子到 Maybe 函子的自然变换：alpha :: Reader () a -\u0026gt; Maybe a 这样的自然变换只有 dumb：dumb (Reader _) = Nothing 与 obvious：obvious (Reader g) = Just (g ()) （用 g 能做的事情仅仅是让它作用于 ()。）\nBeyond Naturality - 超自然性 两个函子之间的参数化多态函数（包括 Const 函子这种边界情况）必定是自然变换。因为所有的标准代数数据类型都是函子，在这些类型之间的任何一个多态函数都是自然变换。我们还掌握了函数类型，它们对于它们的返回类型而言具有着函子性。我们可以使用它们来构造函子（例如 Reader 函子），并为这些函子构造自然变换——更高阶的函数。\n不过，对于参数类型而言，函数类型不具备协变性，它们具备逆变性。当然，逆变函子就是相反范畴中的协变函子。在范畴意义上，两个逆变函子之间的多态函数依然可视为自然变换，除了它们只能作用于 Haskell 类型所构成的两个相反的范畴里的函子。\n之前我们见过的一个逆变函子的示例：newtype Op r a = Op (a -\u0026gt; r) 这个函子对于 a 而言具有逆变性：instance Contravariant (Op r) where contramap f (Op g) = Op (g . f) 我们可以写一个函数，假设它从 Op Bool 到 Op String：predToStr (Op f) = Op (\\x -\u0026gt; if f x then \u0026quot;T\u0026quot; else \u0026quot;F\u0026quot;) 由于这两个函子不具备协变性，它并非 Hask 范畴中的自然变换。不过，由于它们都具备逆变性，所以它们满足『相反的』自然性条件：contramap f . predToStr = predToStr . contramap f 注意，函数 f 必须得走与 fmap 的作用下的方向相反的方向，因为 contramap 的签名是：contramap :: (b -\u0026gt; a) -\u0026gt; (Op Bool a -\u0026gt; Op Bool b)\n存在不是函子的类型构造子吗，它是协变的还是逆变的？看下面的例子：a -\u0026gt; a 这不是一个函子，因为同一类型的 a 出现在负（逆变）位与正（协变）位上。对于这种类型，fmap 或 contramap 都无法实现。因此，函数签名：(a -\u0026gt; a) -\u0026gt; f a 其中 f 是任意函子，这个函数不是自然变换。\nFunctor Category - 函子范畴 现在有了函子之间的映射——自然变换——因此很自然地就会想到，函子是否能够形成范畴？可以。对于每对范畴而言，仅存在一个函子范畴。在这个范畴里，对象是从 C 到 D 的函子，而态射就是这些函子之间的自然变换。\n必须得定义两个自然变换的复合，这相当容易。自然变换的分量是态射，而我们知道怎样实现态射的复合。\n以从函子 F 到函子 G 的自然变换 α 为例。它在对象 a 上的分量是某个态射：α_a :: F a -\u0026gt; G a 打算用对 α 与 β 进行复合，后者是从 G 到 H 的自然变换。β 在 a 上的分量是一个态射：β_a :: G a -\u0026gt; H a 这些态射是可复合的，复合结果又是一个态射：β_a ∘ α_a :: F a -\u0026gt; H a 可以用这个态射作为自然变换 β . α 的分量——自然变换 β 在 a 之后的复合：(β ⋅ α)_a = β_a ∘ α_a 这种复合的结果是从 F 到 H 的自然变换： H f ∘ (β ⋅ α)_a = (β ⋅ α)_b ∘ F f 自然变换的复合遵循结合律，因为它们的分量都是常规态射，而后者的复合是遵循结合律的。最后，对于每个函子 F，存在一个恒等自然变换 1_F，它的分量是恒等态射：id_{F a} :: F a -\u0026gt; F a 所以，函子的确能形成范畴。\n2-Categories - 2-范畴 根据定义，Cat 里的任意 Hom-集都是函子集合。但是两个对象之间的函子有着比集合更丰满的结构。它们形成一个范畴，以自然变换为态射。由于在 Cat 里，函子被认为是态射，自然变换就是态射之间的态射。这个更丰满的结构是 2-范畴的一个例子。2 -范畴是一个广义的范畴，其中，除了对象和态射（这里应该叫 1-态射）之外，还有 2-态射，它就是态射之间的态射。Cat 的 2-范畴具有：对象：（小）范畴; 1-态射：范畴之间的函子; 2-态射：函子之间的自然变换\n用 Hom-范畴——函子范畴 D^C 来代替范畴 C 与 D 之间的 Hom-集。有常规的函子复合：来自 D^C 的函子 F 与来自 E^D 的函子 G 复合，可以得到来自 E^C 的函子 G ∘ F。但是，在每个 Hom-范畴内部，也存在着复合——函子之间自然变换或 2-态射的竖向复合。\n先选在 Cat 里选两个函子，或者 1-态射：F :: C -\u0026gt; D G :: D -\u0026gt; E 与它们的复合：G ∘ F :: C -\u0026gt; E 假设我们有两个自然变换，α 与 β，它们分别作用于函子 F 与 G：α :: F -\u0026gt; F' β :: G -\u0026gt; G' 注意，我们不能对这两个自然变换应用竖向复合，因为 α 的终点与 β 的始点不重合。实际上，它们分别属于两个不同的函子范畴 D^C 与 E^D。不过，我们能够复合函子 F\u0026rsquo; 与 G\u0026rsquo;，因为 F\u0026rsquo; 的终点与 G\u0026rsquo; 的起点重合——它就是范畴 D。函子 G’∘ F’ 与 G ∘ F 之间有什么关系呢？ 这个自然变换称为 α 与 β 的横向复合：β ∘ α :: G ∘ F -\u0026gt; G'∘ F'\nConclusion 对象与范畴是名词；态射、函子以及自然变换是动词。态射连接了对象，函子连接了范畴，自然变换连接了函子。\n已经看到了，一个抽象层上的一个动作，在下一个抽象层次上就变成了一个对象。态射的集合变成了函数对象。作为对象，它可以是另一个态射的始点或终点。这就是高阶函数背后的思想。\n函子，将对象映射为对象，因此我们将其作为类型构造子，或者作为一种参数化的类型。函子，也能将态射映射为态射，因此它也是高阶函数——fmap。有一些简单的函子，例如 Const、积以及余积，它们可以产生大量的代数数据类型。函数类型也具有函子性，协变性与逆变性，它们可以用于扩充代数数据类型。\n函子在函子范畴里可以视为对象。这样，它们就变成了态射的始点与终点，于是有了自然变换。自然变换就是特定形式的多态函数。\n","date":"2020-04-04T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/10.natural-transformations/","title":"1.10 Natural Transformations - 自然变换"},{"content":"Function Type - 函数类型 Function Types\n\u0026lt;译\u0026gt; 函数类型\n曾多次提到函数类型，函数类型与其它普通类型有显著不同。以Integer为例，它是整数的集合；Bool是两个元素构成的集合。然而一个函数类型a-\u0026gt;b的内涵要更大一些，它是从a到b的所有态射构成的集合。从一个对象到另一个对象的所有态射构成的集合，叫hom-集。在集合范畴中，hom-集自身也是个对象。对其它范畴而言，hom-集会形成一个外部范畴，这样的hom-集成为外hom-集。集合的范畴的自引用属性使得函数类型变得有些特殊。但是，至少在某些范畴中，有一种方法可以构造 hom-集这样的对象。这种 hom-集被称为内 hom-集。\nUniversal Construction - 泛构造 先着手尝试去构造一个函数类型，或者从广义上说，去构造一个内 hom-集。\n可将函数类型视为一种复合类型，因为它描述的是一个参数类型（Argument Type）与结果类型（Result Type）之间的关系。用泛构造定义过积类型与余积类型，现可以用同样技巧来定义函数类型，前提是需给出可以联系三种对象的模式。这三种对象分别是：要构造的函数类型，参数类型及结果类型。\n显然，联系这三种类型的模式就是函数应用（Function Application）或求值。对于函数类型，假设存在一个候选者 z（在非集合的范畴中，z 只是个普通的对象），设参数类型为 a（一个对象），函数『应用』可将 z 与 a 构成的序对映射为结果类型 b（一个对象）。现有三个对象，它们中有两个是固定的（一个是参数类型，另一个是结果类型），剩下的那个就是『应用』。『应用』是一种映射，该如何将它融入到我们刚才所建立的模式之中？如果允许查看对象的内部，就可以将一个函数 f（z 的一个元素）与参数 x 封装为序对，然后将这个序对映射为 f x（f 作用于 x，所得结果就是 b 的一个元素）。只是能处理单个的序对 (f, x) 是没有多少意思的，我们要处理的是函数类型的候选者 z 与参数类型 a 的积，即 z×a。这个积是一个对象，可以选择从这个对象到 b 的一个箭头 g 作为态射，g 也就是『应用』。在集合的范畴中，g 就是将每个 (f, x) 映射为 f x 的函数。\n这样，就建立起了这样一个模式：对象 z 与对象 a 的积，通过态射 g 被关联到另一个对象 b。\n以对象与态射构成的模式为起点，它是一种不精确的查询，通常会命中很多东西。特别是在集合的范畴中，几乎每一样东西都与其他东西具有相关性。可以拮取任意一个对象 z，将其与 a 形成积，再找个函数将积映射为 b（除非 b 是空集）。这样，排名（Ranking）就派上了用场，就是检查一下是不是在函数类型的候选者之间存在唯一的映射——可以因式化泛构造的映射。当且仅当存在一个唯一的从 z\u0026rsquo; 到 z 的映射，使得 g\u0026rsquo; 可由 g 的因式来构造，即可判定伴随态射 g（从 z×a 到 b）的 z 比伴随 g\u0026rsquo; 的候选者 z\u0026rsquo; 更好。\n假设存在态射 h:: z\u0026rsquo; -\u0026gt; z，我们想获得从 z\u0026rsquo;×a 到 z×a 的态射。由于积类型本身是一个函子（更确切的说，是二元自函子），因此可以提升一对态射。也就是说，不仅能定义对象的积，也能定义态射的积。由于不需要改变 z\u0026rsquo;×a 这个积的第二个成员 a，因此我们要提升的态射序对是 (h, id)，其中 id 是作用于 a 的恒等态射。现在我们可以建立『应用』的因式了，将 g\u0026rsquo; 表示为 g 的因式：g' = g ∘ (h × id) 这里，关键之处在于态射积的作用\n泛构造的第三个步就是选出最好的那个候选者——称之为 a⇒b（在这里，将它视为一个对象的名字即可，不要与 Haskell 类型类的约束符号混淆，下文会给出其他命名方式）。这个对象伴随的『应用』——从 (a⇒b)×a 到 b 的态射——称之为 eval。如果其他候选者所对应的『应用』g 都能由 eval 被唯一的构造出来，那么 a⇒b 就是最好的那个候选者。\n形式定义如下：\n一个从 a 到 b 的函数对象是伴随着态射eval :: ((a⇒b) × a) -\u0026gt; b的a⇒b，它对于伴随着态射g :: z × a -\u0026gt; b的任意其他对象 z 而言， 存在唯一的态射h :: z -\u0026gt; (a⇒b)， g 可表示为 h 与 eval 所构成的因式：g = eval ∘ (h × id)。\n虽然不能担保对于某个范畴中的任意的对象 a 与 b 都存在着 a⇒b，但是对于集合的范畴却总是存在着这样的 a⇒b，并且在集合的范畴中，a⇒b 与 Hom-集 Set(a,b) 同构。这也是为何在 Haskell 中我们将函数类型 a -\u0026gt; b 解释为范畴意义上的函数对象 a⇒b 的原因。\nCurrying - 柯里化 再次观察函数类型的所有候选者。不过这次将态射 g 视为两个变量 z 与 a 的函数：g :: z × a -\u0026gt; b 一个接受积类型的态射与一个接受两个变量的函数很相似。特别是在集合的范畴中，g 是接受值的序对的函数，其中一个值来自集合 z，另一个来自集合 a。另一方面，泛性质告诉我们，对于每个这样的 g，都存在唯一的态射 h——将 z 映射为一个函数类型 a⇒b：h :: z -\u0026gt; (a⇒b)\n在集合的范畴中，这恰恰意味着 h 是一个接受 z 类型并返回一个从 a 到 b 的函数的函数。也就是说，h 是一个高阶函数。因此这种泛构造在「接受两个变量的函数」与「接受一个变量并返回函数的函数」之间建立了壹壹对应的关系。这种对应关系叫做柯里化，并且 h 叫做 g 的柯里化版本。\n这种对应关系一对一的，因为对于任意 g 都存在唯一的 h，而对于任意的 h，总是能重建一个接受 2 个参数的函数 g，即：g = eval ∘ (h × id) 可将函数 g 称为 h 的反柯里化版本 (uncurried)。\n柯里化是 Haskell 内建的语法。返回一个函数的函数：a -\u0026gt; (b -\u0026gt; c) 经常被视为一个接受两个参数的函数，也就是将上述的函数签名中的括号去掉，即：a -\u0026gt; b -\u0026gt; c 严格的说，接受 2 个参数的函数，它本质上接受的是一个序对（积类型）：(a, b) -\u0026gt; c\n有两个函数可以实现它们的相互转换，这两个函数分别叫 curry 与 uncurry：\n1 2 3 4 5 curry :: ((a, b)-\u0026gt;c) -\u0026gt; (a-\u0026gt;b-\u0026gt;c) curry f a b = f (a, b) uncurry :: (a-\u0026gt;b-\u0026gt;c) -\u0026gt; ((a, b)-\u0026gt;c) uncurry f (a, b) = f a b 注意，curry 是函数类型泛构造的因子生成器，将其写为下面的形式会更清晰一些\n1 2 factorizer :: ((a, b)-\u0026gt;c) -\u0026gt; (a-\u0026gt;(b-\u0026gt;c)) factorizer g = \\a -\u0026gt; (\\b -\u0026gt; g (a, b)) 在非函数式语言中，例如 C++，可以实现柯里化，但不是那么简单。借助模板 std::bind，可实现 C++ 函数的部分应用。\n1 2 3 4 5 6 7 8 std::string catstr(std::string s1, std::string s2) { return s1 + s2; } using namespace std::placeholders; auto greet = std::bind(catstr, \u0026#34;Hello \u0026#34;, _1); std::cout \u0026lt;\u0026lt; greet(\u0026#34;Haskell Curry\u0026#34;); 虽然 Scala 要比 C++ 或 Java 更加的函数化，但是它却做不到函数的部分应用。如果需要函数能被部分应用，必须借助多参数列表：\n1 def catstr(s1: String)(s2: String) = s1 + s2 Exponentials - 指数 在数学领域，从对象 a 到对象 b 的函数或内 hom-对象（hom-集合中的对象）通常称为指数，表示为$b^a$ 。注意，函数参数类型位于指数的位置。\n考虑一下那些建立在有限类型——值的数量为有限的类型，例如 Bool, Char, 甚至 Int 或 Double 之上的函数。至少在理论上，这些函数可被记忆化，亦即可将这些函数转化为表，然后通过查表的方式获得函数返回值。这是函数（态射）与函数类型（对象）之间等价的本质。\n例如，一个接受 Bool 的（纯）函数可以被特化为一对值：一个对应于 False，另一个对应于 True 。比方说，所有从 Bool 到 Int 的函数等价于所有 Int 序对所构成的集合，用积来表示就是 Int × Int，或者再有点创造性，可将其表示为 $Int^2$ $Int^{Bool}$。\n再看一个例子，C++ 的 char 类型，它包含 256 个值。在 C++ 标准库中经常会使用数据查询的方式来定义一些函数。例如 isupper 或 isspace 都是用表来实现的，它等价于 256 个布尔值构成的元组。元组是积类型，因此我们要处理的是 256 个 Bool 值的积：bool × bool × bool × \u0026hellip; × bool。在算术中，重复的积就是幂。如果你将 bool 乘以它自身 256（或 char）次，那么你得到的就是 $bool^{char}$。bool 的 256-元组一共有多少个？答案是 $2^{256}$ 个。这也就是从 char 到 bool 的不同函数的总数，每个函数对应唯一的 256-元组。同理，从 bool 到 char 的函数数量是 $256^2$ 。在这些例子中，函数类型的指数表示相当美妙。\n我们可能并不想将一个接受 int 或 double 的函数完全的记忆化，因为这样不切实际，但是函数与数据类型之间的等价性总是客观存在的。还有一些有限类型，例如列表、字符串或树，如果将接受这些类型的函数进行记忆化，需要无限的存储空间。然而 Haskell 是一种惰性语言，因此在惰性求值的（无限的）数据结构与函数之间的界限并不那么明显。这种函数与数据之间的对偶性揭示了 Haskell 的函数类型与范畴化的指数对象之间的等价性——我们又朝向数据迈进了一步。\nCartesian Closed Categories - 笛卡尔闭范畴 一个笛卡尔闭范畴必须包含：终端对象、任意对象序对的积、任意对象序对的指数。集合范畴就属于此类范畴。\n若能将指数想象为重复的积（可能是无限次），那么你就可以将笛卡尔闭范畴想象为支持任意数量的积运算的范畴。特别是，可将终端对象想象为 0 个对象的积，或者一个对象的 0 次幂。从计算机科学的角度来看，笛卡尔闭范畴的有趣之处在于它为简单的类型 Lambda 演算提供了模型。\n终端对象与积也分别具有对偶物：初始对象与余积。笛卡尔闭范畴也支持后者，积通过分配率可转化为余积：\n1 2 a × (b + c) = a × b + a × c (b + c) × a = b × a + c × a 这样的范畴被称为双向笛卡尔闭范畴(bicartesian closed category)。\nExponentials and Algebraic Data Types - 指数与代数数据类型 从指数的角度来阐释函数类型，这种方式也能很好的适用于代数数据类型。事实上，中学代数中所涉及的 0，1，加法，乘法以及指数等结构，在任何双向笛卡尔闭范畴中同样存在，它们分别对应于初始对象，终端对象，余积，积以及指数等。\nZeroth Power - 0次幂 $a^0 = 1$\n在范畴论中，0 即初始对象，1 即终端对象，『相等』即恒等态射。指数即内 hom-对象。面这个特殊的指数表示的是从初始对象到任意对象 a 的态射集合。基于初始对象的定义，这样的态射只有一个，因此 hom-集 $C(0,a)$ 是一个单例集合。一个单例集合在集合范畴中是终端对象，因此上面这个等式在集合范畴中是成立的，这也意味着它在任何双向笛卡尔闭范畴中都成立。\n在 Haskell 中，用 Void 表示 0，用 unit 类型 () 表示 1，用函数类型表示指数。所有从 Void 到任意类型 a 的函数集合等价于 unit 类型——单例集合。换句话说，有且仅有一个函数 Void -\u0026gt; a，这个函数叫 absurd。不过，这样讲有些不严谨，原因有二。第一，在 Haskell 不存在没有值的类型——每种类型都包含着『永不休止的运算』，即底。第二，absurd 的所有实现都是等价的，因为无论用那种方式实现它们，也没人能够执行它们——没有值可以传递给 absurd。（如果你传递给它一个永不休止的运算，它什么也不会返回！）\nPower of One - 1的幂 $1^a = 1$\n在集合范畴中，这个等式重申了终端对象的定义：从任意对象到终端对象存在唯一的态射。从 a 到终端对象的内 home-对象通常与终端对象本身是同构的。在 Haskell 中，只有一个函数是从任意类型 a 到 unit 类型的，之前见过这个函数—— unit。你可以认为它是 const 函数对 () 的偏应用。\nFirst Power - 1次幂 $a^1 = a$\n这个等式重申了从终端对象出发的态射可用于从对象 a 中拮取元素。这种态射的集合与对象 a 本身是同构的。在集合范畴与 Haskell 中，集合 a 与从 a 中拮取元素的函数 () -\u0026gt; a 是同构的。\nExponentials of Sums - 指数的和 $a^{b+c} = a^c + b^c$\n从范畴论的角度来看，这个等式描述的幂为两个对象的余积的指数与两个指数的积同构。\n在 Haskell 中，这种代数等式有着非常特别的解释，它告诉了我们，两个类型的和的函数与两个参数类型为单一类型的函数的积同构。这恰恰就是我们在定义作用于和类型的函数时所用到的分支分析，也就是说，函数定义中的 case 语句可以用两个或多个处理特定类型的函数来替代。例如，下面这个从和类型 (Eigher Int Double) 出发的函数 f：f :: Either Int Double -\u0026gt; String 可以定义为一个函数对：\n1 2 f (Left n) = if n \u0026lt; 0 then \u0026#34;Negative int\u0026#34; else \u0026#34;Positive int\u0026#34; f (Right x) = if x \u0026lt; 0.0 then \u0026#34;Negative double\u0026#34; else \u0026#34;Positive double\u0026#34; 在此，n 是 Int，而 x 是 Double。\nExponentials of Exponentials - 指数的指数 $(a^b)^c = a^{b \\times c}$\n这个等式表达的是指数对象形式的柯里化——返回一个函数的函数等价与积类型的函数（带两个参数的函数）。\nExponentials over Products - 积的指数 $(a \\times b)^c = a^c \\times b^c$\n在 Haskell 中，返回一个序对的函数与一对函数等价，后者的每个函数都返回序对的一个元素。\nCurry-Howard Isomorphism 柯里-霍华德同构 逻辑学与代数数据类型之间有一些对应。Void 类型与 unit 类型 () 分别对应于错误与正确。积类型与和类型分别对应于逻辑与运算 $\\vee$ 与逻辑或运算 $\\wedge$ 。遵循这一模式，我们所定义的函数类型对应于逻辑推理$\\Longrightarrow$ ，换句话说，类型 a -\u0026gt; b 可以读为『如果 a 那么 b』。\n根据柯里-霍华德同构理论，每种类型可视为一个命题——为真或为假的陈述语句。如果类型是有值的，那么它就是真命题，否则就是伪命题。在实践中，如果一个函数类型有值，亦即存在这样的函数，那么与它对应的逻辑推理就为真。去实现一个函数，就是在证明一个定理。写程序，就等价于证明许多定理。下面看几个例子。\n以函数类型定义中所用的 eval 函数为例，它的签名是：eval :: ((a -\u0026gt; b), a) -\u0026gt; b 它接受一个由函数与其参数构成的序对，产生相应的类型。这个函数是一个态射的 Haskell 实现，该态射为： eval :: (a⇒b) × a -\u0026gt; b 这个态射定义了函数类型 a⇒b（或指数类型$b^a$ ）。运用柯里-霍华德同构理论，可将这个签名转化为逻辑命题：$((a \\Longrightarrow b) \\vee a ) \\Longrightarrow b$ 可将上面这条陈述读为：如果 b 由 a 推出为真，并且 a 为真，那么 b 肯定为真。这就是所谓的肯定前件式。要证明这个定理，只需要实现一个函数，即：eval :: ((a -\u0026gt; b), a) -\u0026gt; b eval (f, x) = f x 如果你给我一个从 a 到 b 的函数 f 以及 a 类型的一个值 x 所构成的序对，我就可以将 f 作用于 x，从而产生 b 类型的一个值。通过实现这个函数，我可以证明 ((a -\u0026gt; b), a) -\u0026gt; b 是有值的。因此，在我们的逻辑中，这一肯定前件式为真。\n结果为假的逻辑命题是怎样的？看这个例子，如果 a 或 b 为真，那么 a 肯定为真：$a \\wedge b \\Longrightarrow b$ 这个命题肯定是错的，因为当 a 为假而 b 为真时，就可以构成一个反例。运用柯里-霍华德同构理论，可将这个命题映射为函数签名：Either a b -\u0026gt; a 根本无法实现这样的函数，因为对于 Right 构造的值而言，无法产生类型为 a 的值。（注意，说的是纯函数）。\n最后，来理解一下 absurd 函数：absurd :: Void -\u0026gt; a 将 Void 视为假，可得：$false \\Longrightarrow a$ 这意味着由谎言可推理出一切（爆炸原理）。对于这个命题（函数），下面用 Haskell 给出的一个证据（实现）：absurd (Void a) = absurd a 其中 Void 的定义如下：newtype Void = Void Void 这是我们惯用的花招，这个定义使得 Void 不可能用于构造一个值，因为你要用它构造一个值，前提是必须先提供这个类型的一个值。这样就可以使得 absurd 永远无法被调用。\n","date":"2020-04-01T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/9.function-type/","title":"1.9 Function Type - 函数类型"},{"content":"Functoriality - 函子性 Functoriality\n\u0026lt;译\u0026gt; 函子性\nBifunctors - 二元函子 函子是Cat（范畴的范畴，the category of categories）中的态射，因此对态射/函数形成的大部分直觉对函子也能成立。例如，一个函数能有两个参数，函子也可以有两个参数，这种函子称为二元函子/Bifunctor。对对象而言，二元函子将每个对象对（一个来自范畴C，另一个来自范畴D）映射为范畴E中的某个对象。也就是说，二元汉字是将范畴C与范畴D的笛卡尔积CxD映射为E。\n函子性也要求一个函子必须能映射态射。二元函子必须将一个态射对（一个来自C另一个来自D）映射为E中的态射。注意，这个态射对，只不过是范畴CxD中的一个态射。若一个态射是在范畴的笛卡尔积中定义的，那么它的行为就是将一对对象映射为另一对对象。这样的态射对可以复合：(f, g) ∘ (f', g') = (f ∘ f', g ∘ g')。这种复合是符合结合律的，并且它也有一个恒等态射，即恒等态射对 (id, id)。因此，范畴的笛卡尔积实际上也是一个范畴。\n将二元函子想象为具有两个参数的函数会更直观一些。要证明二元函子是否是函子，不必借助函子定律，只需独立的考察它的参数即可。如果有一个映射，它将两个范畴映射为第三个范畴，只需证明这个映射相对于每个参数（例如，让另一个参数变成常量）具有函子性，那么这个映射就自然是一个二元函子。对于态射，也可以这样来证明二元函子具有函子性。\n下面用 Haskell 定义一个二元函子。在这个例子中，三个范畴都是同一个：Haskell 类型的范畴。一个二元函子是一个类型构造子，它接受两个类型参数。下面是直接从 Control.Bifunctor 库中提取出来的 Bifunctor 类型类的定义：\n1 2 3 4 5 6 7 class Bifunctor f where bimap :: (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; d) -\u0026gt; f a b -\u0026gt; f c d bimap g h = first g . second h first :: (a -\u0026gt; c) -\u0026gt; f a b -\u0026gt; f c b first g = bimap g id second :: (b -\u0026gt; d) -\u0026gt; f a b -\u0026gt; f a d second = bimap id 类型变量 f 表示二元函子，可以看到有关它的所有类型签名都是作用于两个类型参数。第一个类型签名定义了 bimap 函数，它将两个函数映射为一个被提升了的函数 (f a b -\u0026gt; f c d)，后者作用于二元函子的类型构造子所产生的类型。 bimap 有一个默认的实现，即 first 与 second 的复合，这表明只要 bimap 分别对两个参数都具备函子性，就意味着它是一个二元函子。其他两个类型签名是 first 与 second，他们分别作用于 bimap 的第一个与第二个参数，因此它们是 f 具有函子性的两个 fmap 证据。上述类型类的定义以 bimap 的形式提供了 first 与 second 的默认实现。\n当声明 Bifunctor 的一个实例时，可以去实现 bimap，这样 first 与 second 就不用再实现了；也可以去实现 first 与 second，这样就不用再实现 bimap 了。当然，也可以三个都实现了，但是需要确定它们之间要满足类型类的定义中的那些关系。\nProduct and Coproduct Bifunctors - 积与余积二元函子 二元函子的一个重要的例子是范畴积——由泛构造（Universal Construction）定义的两个对象的积。如果任意一对对象之间存在积，那么从这些对象到积的映射就具备二元函子性，这通常是正确的，特别是在 Haskell 中。\n序对构造子就是一个 Bifunctor 实例——最简单的积类型：\n1 2 3 4 5 instance Bifunctor (,) where -- bimap :: (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; d) -\u0026gt; (a, b) -\u0026gt; (c, d) bimap f g (x, y) = (f x, g y) (,) a b = (a, b) 余积作为对偶，如果它作用于范畴中的每一对对象，那么它也是一个二元函子。在 Haskell 中，余积二元函子的例子是 Either 类型构造子，它是 Bifunctor 的一个实例：\n1 2 3 4 instance Bifunctor Either where -- bimap :: (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; d) -\u0026gt; (a, b) -\u0026gt; (c, d) bimap f _ (Left x) = Left (f x) bimap _ g (Right y) = Right (g y) Functorial Algebraic Data Types - 具有函子性的代数数据类型 复杂的数据类型是由简单的数据类型构造出来的。特别是代数数据类型（ADT），它是由和与积创建的。基于和与积的函子性，也了解了函子的复合。因此，若能揭示代数数据类型的基本构造块是具备函子性的，那么就可以确定代数数据类型也具备函子性。\n参数化的代数数据类型的基本构造块是什么？首先，有些构造块是不依赖于函子所接受的类型参数的，例如 Maybe 中的 Nothing，List 中的 Nil。它们等价于 Const 函子。记住，Const 函子忽略它的类型参数（实际上是忽略第二个类型参数，第一个被保留作为常量）。\n其次，有些构造块简单的将类型参数封装为自身的一部分，例如 Maybe 中的 Just，它们等价于恒等函子。之前提到过恒等函子，它是 Cat 范畴中的恒等态射，不过 Haskell 未对它进行定义。给出它的定义(可将 Indentity 视为最简单的容器，它只存储类型 a 的一个（不变）的值。)：\n1 2 3 4 data Identity a = Identity a instance Functor Identity where fmap f (Identity x) = Identity (f x) 其他的代数数据结构都是使用这两种基本类型的和与积构建而成。基于此，从一个新的角度来看 Maybe 类型构造子：data Maybe a = Nothing | Just a 它是两种类型的和，现在知道求和运算是具备函子性的。第一部分，Nothing 可以表示为作用于类型 a 的 Const ()（Const 的第一个类型参数是 unit），而第二部分不过是恒等函子的化名而已。在同构的意义下，我们可以将 Maybe 定义为：type Maybe a = Either (Const () a) (Identity a) 因此，Maybe 是 Const () 函子与 Indentity 函子被二元函子 Either 复合后的结果。Const 本身也是一个二元函子，只不过在这里用的是它的偏应用形式。\n两个函子的复合后，其结果是一个函子。还需要做的就是描述两个函子被一个二元函子复合后如何作用于态射。对于给定的两个态射，我们可以分别用这两个函子对其进行提升，然后再用二元函子去提升这两个被提升后的态射所构成的序对。\n可以在 Haskell 中表示这种复合。先定义一个由二元函子 bf 参数化的数据类型，两个函子 fu 与 gu，以及两个常规类型 a 与 b。我们将 fu 作用于 a，将 gu 作用于 b，然后将 bf 作用于 fu a 与 fu b：newtype BiComp bf fu gu a b = BiComp (bf (fu a) (gu b)) 这是对象的复合，在 Haskell 中也就是类型的复合。(在 Haskell 中，类型构造子作用于类型，就像函数作用于它的参数一样)。考虑将 BiComp 作用于 Either，Const ()，Indentity，a，b。得到的是一个裸奔版本的 Maybe b（a 被忽略了）。\n如果 bf 是一个二元函子，fu 与 gu 都是函子，那么这个新的数据类型 BiComp 就是 a 与 b 之间的二元函子。编译器必须知道与 bf 匹配的 bimap 的定义，以及分别与 fu 与 gu 匹配的 fmap 的定义。在 Haskell 中，这个条件可以预先给出：一个类约束集合后面尾随一个粗箭头：\n1 2 3 4 5 6 7 8 instance (Bifunctor bf, Functor fu, Functor gu) =\u0026gt; Bifunctor (BiComp bf fu gu) where bimap f1 f2 (BiComp x) = BiComp ((bimap (fmap f1) (fmap f2)) x) -- x :: bf (fu a) (gu b) -- f1 :: a -\u0026gt; a\u0026#39; -- f2 :: b -\u0026gt; b\u0026#39; -- bimap (fu a -\u0026gt; fu a\u0026#39;) -\u0026gt; (gu b -\u0026gt; gu b\u0026#39;) -\u0026gt; bf (fu a) (gu b) -\u0026gt; bf (fu a\u0026#39;) (gu b\u0026#39;) 没有必要去证明 Maybe 是一个函子，由于它是两个基本的函子求和后的结果，因此 Maybe 自然也就具备了函子性。\n代数数据结构的规律性不仅适用于 Functor 的自动继承，也适合其它的类型类，例如之前提到的 Eq 类型类。\n对于代数数据类型而言，Functor 实例的继承相当繁琐，这个过程有无可能由编译器自动完成？的确，编译器能做到这一点。你需要在代码的首部中启用 Haskell 的扩展：{-# LANGUAGE DeriveFunctor #-} 然后在数据结构中添加 deriving Functor： data Maybe a = Nothing | Just a deriving Functor 然后就会得到相应的 fmap 的实现\nFunctors in C++ - C++ 中的函子 1 2 3 4 5 6 data Tree a = Leaf a | Node (Tree a) (Tree a) deriving Functor instance Functor Tree where fmap f (Leaf a) = Leaf (f a) fmap f (Node t t\u0026#39;) = Node (fmap f t) (fmap f t\u0026#39;) 基于dynamic_cast 替代模式匹配\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 template\u0026lt;class T\u0026gt; struct Tree { virtual ~Tree() {}; // 为了支持动态类型转换，基类至少需要定义一个虚函数，因此将析构函数定义为虚函数（在任何情况下这都是个好主意） }; template\u0026lt;class T\u0026gt; struct Leaf : public Tree\u0026lt;T\u0026gt; { T _label; Leaf(T l) : _label(l) {} }; template\u0026lt;class T\u0026gt; struct Node : public Tree\u0026lt;T\u0026gt; { Tree\u0026lt;T\u0026gt; * _left; Tree\u0026lt;T\u0026gt; * _right; Node(Tree\u0026lt;T\u0026gt; * l, Tree\u0026lt;T\u0026gt; * r) : _left(l), _right(r) {} }; template\u0026lt;class A, class B\u0026gt; Tree\u0026lt;B\u0026gt; * fmap(std::function\u0026lt;B(A)\u0026gt; f, Tree\u0026lt;A\u0026gt; * t) { Leaf\u0026lt;A\u0026gt; * pl = dynamic_cast \u0026lt;Leaf\u0026lt;A\u0026gt;*\u0026gt;(t); if (pl) return new Leaf\u0026lt;B\u0026gt;(f (pl-\u0026gt;_label)); Node\u0026lt;A\u0026gt; * pn = dynamic_cast\u0026lt;Node\u0026lt;A\u0026gt;*\u0026gt;(t); if (pn) return new Node\u0026lt;B\u0026gt;( fmap\u0026lt;A\u0026gt;(f, pn-\u0026gt;_left) , fmap\u0026lt;A\u0026gt;(f, pn-\u0026gt;_right)); return nullptr; } The Writer Functor - Writer 函子 在 Kleisli 范畴中，态射被表示为被装帧过的函数，返回 Writer 数据结构。type Writer a = (a, String) 这种装帧与自函子有一些关系，并且 Writer 类型构造子对于 a 具有函子性。不需要去为它实现 fmap，因为它只是一种简单的积类型。\nKleisli 范畴与一个函子之间存在什么关系呢？一个 Kleisli 范畴，作为一个范畴，它定义了复合与恒等。复合是通过小鱼运算符实现的,恒等态射是一个叫做 return 的函数\n1 2 3 4 5 6 7 8 (\u0026gt;=\u0026gt;) :: (a -\u0026gt; Writer b) -\u0026gt; (b -\u0026gt; Writer c) -\u0026gt; (a -\u0026gt; Writer c) m1 \u0026gt;=\u0026gt; m2 = \\x -\u0026gt; let (y, s1) = m1 x (z, s2) = m2 y in (z, s1 ++ s2) return :: a -\u0026gt; Writer a return x = (x, \u0026#34;\u0026#34;) 仔细审度这两个函数的类型，结果会发现，可以将它们组合成一个函数，这个函数就是 fmap：fmap f = id \u0026gt;=\u0026gt; (\\x -\u0026gt; return (f x))。这里，小鱼运算符组合了两个函数：一个是 id，另一个是一个匿名函数，它将 return 作用于 f x。最难理解的地方可能就是 id 的用途。小鱼运算符难道不是接受一个『常规』类型，返回一个经过装帧的类型吗？实际上并非如此。没人说 a -\u0026gt; Writer b 中的 a 必须是一个『常规』类型。它是一个类型变量，因此它可以是任何东西，特别是它可以是一个被装帧的类型，例如 Writer b。因此，id 将会接受 Writer a，然后返回 Writer a。小鱼运算符就会拿到 a 的值，将它作为 x 传给那个匿名函数。在匿名函数中，f 会将 x 变成 b，然后 return 会对 b 进行装帧，从而得到 Writer b。把这些放到一起，最终就得到了一个函数，它接受 Writer a，返回 Writer b，这正是 fmap 想要的结果。\n上述讨论是可以推广的：你可以将 Writer 替换为任何一个类型构造子。只要这个类型构造子支持一个小鱼运算符以及 return，那么你就可以定义 fmap。因此，Kleisli 范畴中的这种装帧，实际上是一个函子。（尽管并非每个函子都能产生一个 Kleisli 范畴）\n刚才定义的 fmap 是否与编译器使用 deriving Functor 自动继承来的 fmap 相同？它们是相同的。这是 Haskell 实现多态函数的方式所决定的。这种多态函数的实现方式叫做参数化多态，它是所谓的免费定理（Theorems for free）之源。这些免费的定理中有一个是这么说的，如果一个给定的类型构造子具有一个 fmap 的实现，它能维持恒等（将一个范畴中的恒等态射映射为另一个范畴中的恒等态射），那么它必定具备唯一性。\nCovariant and Contravariant Functors - 协变与逆变函子 现在来回顾 Reader 函子。Reader 函子是『函数箭头』类型构造子的的偏应用（译注：函数箭头 -\u0026gt; 本身就是一个类型构造子，它接受两个类型参数）。(-\u0026gt;) r 可以给它取一个类型别名：type Reader r a = r -\u0026gt; a 将它声明为 Functor 的实例，跟之前我们见过的类似：instance Functor (Reader r) where fmap f g = f . g\n但是，函数类型构造子接受两个类型参数，这一点与序对或 Either 类型构造子相似。序对与 Either 对于它们所接受的参数具备函子性，因此它们二元函子。函数类型构造子也是一个二元函子吗？\n试让函数类型构造子对于第一个参数具备函子性。为此需要再定义一个类型别名——与 Reader 相似，只是参数次序颠倒了一下：type Op r a = a -\u0026gt; r 将返回类型 r 固定了下来，只让参数类型是 a 可变的。与它相匹配的 fmap 的类型签名如下： fmap :: (a -\u0026gt; b) -\u0026gt; (a -\u0026gt; r) -\u0026gt; (b -\u0026gt; r) 只凭借 a -\u0026gt; b 与 a -\u0026gt; r 这两个函数，显然无法构造 b -\u0026gt; r！如果我们以某种方式将第一个函数的参数翻转一下，让它变成 b -\u0026gt; a，这样就可以构造 b -\u0026gt; r 了。虽然我们不能随便反转一个函数的参数，但是在相反的范畴中可以这样做。\n对于每个范畴$C$，存在一个对偶范畴$C^{op}$，后者所包含的对象与前者相同，但是后者所有的箭头都与前者相反。假设 $C^{op}$ 与另一个范畴 $D$ 之间存在一个函子：$F :: C^{OP} \\times D$ 这种函子将 $C^{OP}$ 中的一个态射 $f^{OP} :: a \\rightarrow b$ 映射为 $D$ 中的一个态射 $F f^{OP} :: F a \\rightarrow F b$ . 但是，态射 $f^{op}$ 在原范畴 $C$ 中与某个态射 $f :: b \\rightarrow a$ 相对应，它们的方向是相反的。\nF是一个常规的函子，基于F定义一个映射，这个映射不是函子，称之为G. 这个映射从C到D，映射对象时功能与F相同，作用于态射时，它会将态射的方向反转。它接受C中的一个态射$f :: b \\rightarrow a$，将其映射为相反的态射 $f^{OP} :: a \\rightarrow b$，然后用函子F作用于这个被反转的态射，得到 $F f^{OP} :: F a \\rightarrow F b$。假设 F a与G a相同，F b 与 G b 相同，整个过程可描述为， $G f :: (b \\rightarrow a) \\rightarrow (G a \\rightarrow G b)$。\n这是一个『带有一个扭结的函子』。是范畴的一个映射，它反转了态射的方向，这种映射被称为逆变函子。注意，逆变函子只不过来自相反范畴的一个常规函子。顺便说一下，这种常规函子——我们已经碰到很多了——被称为协变函子。\n下面是 Haskell 中逆变函子的类型类的定义（实际上，是逆变自函子）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Contravariant f where contramap :: (b -\u0026gt; a) -\u0026gt; (f a -\u0026gt; f b) -- Op is an instance instance Contravariant (Op r) where -- (b -\u0026gt; a) -\u0026gt; Op r a -\u0026gt; Op r b contramap f g = g . f flip :: (a -\u0026gt; b -\u0026gt; c) -\u0026gt; (b -\u0026gt; a -\u0026gt; c) flip f y x = f x y -- ==\u0026gt; contramap = flip (.) Profunctors - 副函子 已经看到了函数箭头运算符对于它的第一个参数是具有逆变函子性，而对于它的第二个参数则具有协变函子性。如果是集合范畴，这种东西叫副函子（Profunctor）。由于一个逆变函子等价于相反范畴的协变函子，因此可以这样定义一个副函子：$C^{OP} \\times D \\rightarrow Set$。因为 Haskell 的类型与集合差不多，所以我们可将 Profunctor 这个名字应用于一个类型构造子 p，它接受两个参数，它对于第一个参数具有逆变函子性，对于第二个参数则具有协变函子性。\n1 2 3 4 5 6 7 class Profunctor p where dimap :: (a -\u0026gt; b) -\u0026gt; (c -\u0026gt; d) -\u0026gt; p b c -\u0026gt; p a d dimap f g = lmap f . rmap g lmap :: (a -\u0026gt; b) -\u0026gt; p b c -\u0026gt; p a c lmap f = dimap f id rmap :: (b -\u0026gt; c) -\u0026gt; p a b -\u0026gt; p a c rmap = dimap id 这三个函数只是默认的实现。就像 Bifunctor 那样，当声明 Profunctor 的一个实例时，你要么去实现 dimap，要么去实现 lmap。现在，我们宣称函数箭头运算符是 Profunctor 的一个实例了：\n1 2 3 4 instance Profunctor (-\u0026gt;) where dimap ab cd bc = cd . bc . ac lmap = flip (.) rmap = (.) The Hom-Functor - Hom-函子 hom-set C(a, b) 是个从范畴$C^{OP} \\times C$ 到集合范畴 Set 的函子。$C^{OP} \\times C$中的态射是一对C上的态射 f :: a' -\u0026gt; a g :: b' -\u0026gt; b 这对态射的“提升”版本必定是集合C(a, b)到集合C(a\u0026rsquo;, b\u0026rsquo;)的态射/函数。从C(a,b)中取一个元素h h :: a -\u0026gt; b，然后 g ∘ h ∘ f便是集合C(a\u0026rsquo;, b\u0026rsquo;)中的元素。\n如上所见，hom-函子是副函子的一个特例。\n","date":"2020-03-26T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/8.functoriality/","title":"1.8 Functoriality - 函子性"},{"content":"Link to Blog\n\u0026lt;译\u0026gt; 函子\nFunctors - 函子 函子是范畴之间的映射。给定两个范畴C与D，函子F可以将C中的对象映射为D中的对象，可以将C中态射映射为D中态射，并保持其结构。若C中有对象a，它在D中的象为Fa；C中有从对象a到b的态射f，其在D中的象为Ff，从Fa到Fb。此外，态射的复合也应当符合直觉的，在范畴C中h是g与f的复合，那么在范畴D中，Fh应当是Fg与Ff的复合。最后，C中的恒等态射被映射为D中的恒等态射。$Fid_a = id_{Fa}$ (F ida = idFa) ida 是作用于对象a的恒等态射，idFa是作用于对象Fa的恒等态射。\n函子比常规函数的约束更为严格，函子必须保持范畴的结构。\n函子可以做折叠或嵌入的工作。所谓嵌入，就是将一个小的源范畴嵌入到更大的目标范畴中。一个极端的例子，源范畴是个单例范畴——只有一个对象与一个态射（恒等态射）的范畴，从单例范畴映射到任何其他范畴的函子，所做的工作就是在后者中选择一个对象。这完全类似于接受单例集合的态射，这个态射会从目标集合中选择元素。最巨大的折叠函子被称为常函子$\\triangle_C$，它将源范畴中的每个对象映射为目标范畴中特定的对象 c，它也可以将源范畴中的每个态射映射为目标范畴中的特定的恒等态射$id_c$，它在行为上像一个黑洞，将所有东西压成一个奇点。在讨论极限与余极限时，我们再来考察这个黑洞函子。\nFunctors in Programming - 编程中的函子 functors that map this category into itself — such functors are called endofunctors.\n函子将这个范畴映射为其自身——这样的函子被称为自函子。\nMaybe Functor - Maybe 函子 Maybe 的定义就是将类型 a 映射为类型 Maybe a：data Maybe a = Nothing | Just a。\nMaybe 本身不是一个类型，它是一个类型构造子（Constructor）。必须向它提供一个类型参数，例如 Int 或 Bool，然后才可以使其变成一个类型。如不果不向 Maybe 提供任何参数，那么它就是一个作用于类型的函数。\n一个函子不仅仅只映射对象（在此，是类型），它也映射态射（在此，是函数）。对于任何从 a 到 b 的函数：f :: a -\u0026gt; b被 Maybe 函子映射为：f' :: Maybe a -\u0026gt; Maybe b。Haskell 以高阶函数的形式实现了一个函子的态射映射部分，这个函数叫 fmap。对于 Maybe 的情况，这个函数的签名如下：fmap :: (a -\u0026gt; b) -\u0026gt; (Maybe a -\u0026gt; Maybe b)。通常说fmap提升（Lift）了一个函数。被提升的函数作用于 Maybe 层次上的值。\n为了说明类型构造子 Maybe 携同函数 fmap 共同形成一个函子，不得不证明 fmap 能够维持恒等态射以及态射的复合的存在。所证明的东西，叫做『函子定律』。凡是满足函子定律的函子，必定不会破坏范畴的结构。\nEquational Reasoning - 等式推导 为了证明函子定律，我需要借助等式推导，这也是 Haskell 中常用的证明技巧。它利用了 Haskell 函数基于等式定义这一优势：左侧等于右侧。总是可以用其中一侧替换另一侧，只是有时变量名需要改一下以避免名字冲突。可以将这种替换视为内联一个函数，或者将一个表达式重构为一个函数。这种替换可以从两个方向进行，如果一个函数是基于模式匹配定义的，可以单独使用它的子定义。\n先从证明函子对恒等态射的维持开始：fmap id = id。要考虑两种情况：Nothing 与 Just。\n1 2 3 4 5 6 7 8 9 10 11 12 13 fmap id Nothing = { definition of fmap } Nothing = { definition of id } id Nothing fmap id (Just x) = { definition of fmap } Just (id x) = { definition of id } Just x = { definition of id } id (Just x) 现在来证明 fmap 能够维持态射的复合：fmap (g . f) = fmap g . fmap f\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fmap (g . f) Nothing = { definition of fmap } Nothing = { definition of fmap } fmap g Nothing = { definition of fmap } fmap g (fmap f Nothing) fmap (g . f) (Just x) = { definition of fmap } Just ((g . f) x) = { definition of composition } Just (g (f x)) = { definition of fmap } fmap g (Just (f x)) = { definition of fmap } fmap g (fmap f (Just x)) = { definition of composition } (fmap g . fmap f) (Just x) Optional Maybe一般其它语言中一般是Optional来实现。给出C++中的伪实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 template\u0026lt;class T\u0026gt; class optional { bool _isValid; // the tag T _v; public: optional() : _isValid(false) {} // Nothing optional(T x) : _isValid(true) , _v(x) {} // Just bool isValid() const { return _isValid; } T val() const { return _v; } }; // fmap - v1 template\u0026lt;class A, class B\u0026gt; std::function\u0026lt;optional\u0026lt;B\u0026gt;(optional\u0026lt;A\u0026gt;)\u0026gt; fmap(std::function\u0026lt;B(A)\u0026gt; f) { return [f](optional\u0026lt;A\u0026gt; opt) { if (!opt.isValid()) return optional\u0026lt;B\u0026gt;{}; else return optional\u0026lt;B\u0026gt;{ f(opt.val()) }; }; } // fmap -v2 template\u0026lt;class A, class B\u0026gt; optional\u0026lt;B\u0026gt; fmap(std::function\u0026lt;B(A)\u0026gt; f, optional\u0026lt;A\u0026gt; opt) { if (!opt.isValid()) return optional\u0026lt;B\u0026gt;{}; else return optional\u0026lt;B\u0026gt;{ f(opt.val()) }; } Type Class - 类型类 Haskell使用类型类对函子进行抽象。一个类型类定义了支持一个公共接口的类型族。例如，支持相等谓词的类型类如下\n1 2 class Eq a where (==) :: a -\u0026gt; a -\u0026gt; Bool 这个定义陈述的是，如果类型 a 支持 (==) 运算符，那么它就是 Eq 类。(==) 运算符接受两个类型为 a 的值，返回 Bool 值。如果你想告诉 Haskell 有一个特定的类型是 Eq 类，那么你不得不将其声明为这个类的一个实例，并提供 (==) 的实现。例如，一个二维 Point（两个 Float 的积类型）：data Point = Pt Float Float 需要为它定义相等谓词：\n1 2 instance Eq Point where (Pt x y) == (Pt x\u0026#39; y\u0026#39;) = x == x\u0026#39; \u0026amp;\u0026amp; y == y\u0026#39; 这里将 (==) 作为中缀运算符使用，它处于 (Pt x y) 与 (Pt x\u0026rsquo; y\u0026rsquo;) 之间，而函数体是单个 = 号后面的部分。一旦将 Point 声明为 Eq 的一个实例，你就可以直接比较点与点是否相等了。注意，与 C++ 或 Java 不同，在定义 Point 的时候不必指定它是 Eq 类（或接口）的实例——可在真正需要的时候再指定。\n类型类是 Haskell 仅有的函数（运算符）重载机制。在为不同的函子重载 fmap 时需要借助类型类，尽管有一个难点：函子不能作为类型来定义，只能作为类型的映射来定义，即类型构造子。我们需要一个由类型构造子构成的族，而不是像 Eq 这样的类型族。所幸，Haskell 的类型类可以将类型构造子像类型那样来处理。因此，Functor 类的定义如下：\n1 2 class Functor f where fmap :: (a -\u0026gt; b) -\u0026gt; f a -\u0026gt; f b 如果存在符合上述签名的 fmap 函数，这个类规定了 f 是一个 Functor。小写的 f 是一个类型变量，类似于类型变量 a 与 b，然而编译器能够推断出它是一个类型构造子，而不是一个类型，依据是它的用途：作用于其他类型，即 f a 与 f b。因此，要声明一个 Functor 的实例时，你不得不给它一个类型构造子，对于 Maybe 而言就是：\n1 2 3 instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just x) = Just (f x) 顺便说一下，Functor 类，以及它的一些实例，这些实例是为大多数简单的数据类型而定义的，包括 Maybe，它们都是 Haskell 标准库的一部分。\nFunctor in C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template\u0026lt;template\u0026lt;class\u0026gt; F, class A, class B\u0026gt; F\u0026lt;B\u0026gt; fmap(std::function\u0026lt;B(A)\u0026gt;, F\u0026lt;A\u0026gt;); // error! - C++ 中是禁止模板函数的部分特化 template\u0026lt;class A, class B\u0026gt; optional\u0026lt;B\u0026gt; fmap\u0026lt;optional\u0026gt;(std::function\u0026lt;B(A)\u0026gt; f, optional\u0026lt;A\u0026gt; opt) // - 只能这样实现 template\u0026lt;class A, class B\u0026gt; optional\u0026lt;B\u0026gt; fmap(std::function\u0026lt;B(A)\u0026gt; f, optional\u0026lt;A\u0026gt; opt) { if (!opt.isValid()) return optional\u0026lt;B\u0026gt;{}; else return optional\u0026lt;B\u0026gt;{ f(opt.val()) }; } List Functor data List a = Nil | Cons a (List a) List 是类型构造子，它将任意类型 a 映射为类型 List a。为了表明 List 是一个函子，我们不得不定义一个提升函数：接受一个 a -\u0026gt; b 的函数，产生一个 List a -\u0026gt; List b 的函数：fmap :: (a -\u0026gt; b) -\u0026gt; (List a -\u0026gt; List b) 列表函子的实例声明：\n1 2 3 instance Functor List where fmap _ Nil = Nil fmap f (Cons x t) = Cons (f x) (fmap f t) Reader Functor 已经对函子获得一些直觉了——例如，函子是某种容器。下面来个有点烧脑的例子，考虑一个从类型 a 到一个返回 a 的函数类型的映射。\n在 Haskell 中，函数类型是使用箭头类型构造子 (-\u0026gt;) 构造出来的，这个类型构造子接受两种类型：参数类型与返回类型。你已经见过这个类型构造子的中缀形式 a -\u0026gt; b，但是也可以写成前缀形式，像是被参数化了：(-\u0026gt;) a b。就像常规的函数一样，接受多个参数的类型函数可以偏应用。因此，当我们向箭头只提供一个参数时，它依然期待另一个参数的出现。因此 (-\u0026gt;) a也是个类型构造子。它需要一个类型 b 来产生完整的类型 a -\u0026gt; b。它所表示的是，它定义了一族由 a 参数化的类型构造子。\n处理两个类型参数可能有点混乱，先做一些重命名的工作。在我们之前的函子定义中，我们可以将参数类型称为 r，将返回类型称为 a。因此我们的类型构造子可以接受任意类型 a，并将其映射为类型 r -\u0026gt; a。为了表明它是个函子，我们就需要一个函数，它可以将函数 a -\u0026gt;b 提升为一个从 r -\u0026gt; a 到 r -\u0026gt; b 的函数，而 r -\u0026gt; a 与 r -\u0026gt; b 就是 (-\u0026gt;) r 这个类型构造子分别作用于 a 与 b 所产生的函数类型。以上讨论最终可归结为 fmap 的函数签名：fmap :: (a -\u0026gt; b) -\u0026gt; (r -\u0026gt; a) -\u0026gt; (r -\u0026gt; b) 解决一个难题：对于给定的函数 f :: a -\u0026gt; b 与 g :: r -\u0026gt; a，构造一个函数 r -\u0026gt; b。这是复合两个函数的唯一途径，也恰恰就是我们需要的。因此，我们需要将 fmap 的实现为：\n1 2 instance Functor ((-\u0026gt;) r) where fmap f g = f . g 就是我们想要的 fmap，紧凑些的表示fmap f g = (.) f g ，甚至 fmap = (.)\n类型构造子 (-\u0026gt;) r 与上面这个 fmap 组合起来所形成的函子被称为 Reader 函子。\nFunctors as Containers - 函子作为容器 纯函数是可以被保存下来的，函数的执行结果被扔到可检索的表中，而表是数据。 Haskell 具有惰性计算能力，一个传统的容器，譬如一个列表，实际上可以被实现为一个函数。例如，一个包含自然数的无限长的列表，可被定义为：nats :: [Integer] = [1..] 显然，一个无限长的列表是不能存储在内存中的。编译器将其实现为一个可以按需产生一组 Integer 值的函数。Haskell 有效的模糊了数据与代码的区别。可以将列表视为函数，也可以将函数视为从存储着参数与结果的表中查询数据。如果函数的定义域有界并且不太大，将函数变成表查询是完全可行的。\n将函子对象（由自函子产生的类型的实例）视为包含着一个值或多个值的容器，即使这些值实际上并未出场。C++ 中的 std::future 函子在某些时间点上可以存储一个值，但是它不能担保这个值总是存在；如果你想访问这个值，可能会受阻，直到其它线程的计算过程。另一个例子是 Haskell 的 IO 对象，它包含着用户的输入，或者是我们的宇宙要显示于屏幕上的『Hello World!』的未来版本。根据这种解释，函子对象就是可以包含一个值或多个值的容器，这些值的类型参数化了函子对象。或者，函子对象可能包含产生这些值的方法。我们根本不关心能否访问这些值——这些事发生在函子作用范围之外。如果函子对象包含的值能够被访问，我们就可以看到相应的操作结果；如果它们不能被访问，我们所关心的只是操作的正确复合，以及伴随不改变任何事物的恒等函数的操作。\n为了向你表明我们是如何的不关心函子对象内部的值，这里有一个类型构造子，它完全的忽略参数 a：data Const c a = Const c Const 类型构造子接受两种类型， c 与 a。就像我们处理箭头构造子那样，我们对其进行偏应用从而制造了一个函子。数据构造子（也叫 Const）仅接受 c 类型的值，它不依赖 a。与这种类型构造子相配的 fmap 类型为：fmap :: (a -\u0026gt; b) -\u0026gt; Const c a -\u0026gt; Const c b 因为这个函子是忽略类型参数的，所以 fmap 的实现也可以自由忽略那个函数参数——因为这个函数无事可做：\n1 2 instance Functor (Const c) where fmap _ (Const v) = Const v 在 C++ 中可能更清晰一些，因为 C++ 中在类型参数与值之间有着明显的区别，前者出现于编译期间，后者出现于运行时：\n1 2 3 4 5 6 7 8 9 10 template\u0026lt;class C, class A\u0026gt; struct Const { Const(C v) : _v(v) {} C _v; }; template\u0026lt;class C, class A, class B\u0026gt; Const\u0026lt;C, B\u0026gt; fmap(std::function\u0026lt;B(A)\u0026gt; f, Const\u0026lt;C, A\u0026gt; c) { return Const\u0026lt;C, B\u0026gt;{c._v}; } 尽管它有些怪异，但是 Const 函子在许多结构中扮演着重要的角色。在范畴论中，它是$\\triangle_C$函子的特例，后者我们在前面提到过的，就是那个黑洞函子，而 Const 是个黑洞自函子。\nFunctor Composition 函子的复合类似于集合之间的函数复合。两个函子的复合，就是两个函子分别对各自的对象进行映射的复合，对于态射也是这样。恒等态射穿过两个函子之后，它还是恒等态射。复合的态射穿过两个函子之后还是复合的态射。自函子很容易复合。\n1 2 3 maybeTail :: [a] -\u0026gt; Maybe [a] maybeTail [] = Nothing maybeTail (x:xs) = Just xs maybeTail 返回的结果是两个作用于 a 的函子 Maybe 与 [] 复合后的类型。这些函子，每一个都配备了一个 fmap，但是如果我们想将一个函数 f 作用于复合的函子 Maybe [] 所包含的内容，该怎么做？我们不得不突破两层函子的封装：使用 fmap 突破 Maybe，再使用 fmap 突破列表。例如，要对一个 Maybe [Int] 中所包含的元素求平方，可以这样做：\n1 2 3 4 5 6 square x = x * x mis :: Maybe [Int] mis = Just [1, 2, 3] mis2 = fmap (fmap square) mis 经过类型分析，对于外部的 fmap，编译器会使用 Maybe 版本的；对于内部的 fmap，编译器会使用列表版本的。于是，上述代码也可以写为：mis2 = (fmap . fmap) square mis 要记住，fmap 可以看作是只接受一个参数的函数： fmap :: (a -\u0026gt; b) -\u0026gt; (f a -\u0026gt; f b) 示例中，(fmap . fmap) 中的第 2 个 fmap 所接受的参数是： square :: Int -\u0026gt; Int 然后返回一个这种类型的函数： [Int] -\u0026gt; [Int] 第一个 fmap 接受这个函数，然后再返回一个函数： Maybe [Int] -\u0026gt; Maybe [Int] 最终，这个函数作用于 mis。因此两个函子的复合结果，依然是函子，并且这个函子的 fmap 是那两个函子对应的 fmap 的复合。\n现在回到范畴论：显然函子的复合是遵守结合律的，因为对象的映射遵守结合律，态射的映射也遵守结合律。在每个范畴中也有一个恒等函子：它将每个对象都映射为其自身，将每个态射映射为其自身。因此在某个范畴中，函子具有与态射相同的性质。什么范畴会是这个样子？必须得有一个范畴，它包含的对象是范畴，它包含的态射是函子。也就是说，它是范畴的范畴。但是，所有范畴的范畴不得不包含它自身，这样我们就陷入了自相矛盾的境地，就像不可能存在集合的集合那样。然而，有一个叫做 Cat 的范畴，它包含了所有的小范畴。这个范畴是一个大的范畴，因此它就不可能是它自身的成员。所谓的小范畴，就是它包含的对象可以形成一个集合，而不是某种比集合还大的东西。请注意，在范畴论中，即使一个无限的不可数的集合也被认为是『小』的。我想我已经提到过这样的集合了，因为我们已经看过同样的结构在许多抽象层次上的重复出现。以后我们也会看到函子也能形成范畴。\nChallenge 1 let fmap = (\u0026lt;\u0026lt;) ","date":"2020-03-21T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/7.functors/","title":"1.7 Functors - 函子"},{"content":"Simple Algebraic Data Types - 简单代数类型 从很多地方看到，函数式世界里两类数据类型，和类型 积类型。参考之前的视角，将编程语言的类型视为集合，类型的实例便是集合中的元素。笛卡尔积式的组合方式，便是积类型，枚举式的组合，便是和类型。（从得到的数据个数上看，积类型最终得到m*n个元素，而和类型只能得到m+n个）\nProduct Types - 积类型 积类型直接体现是元组/序对，元组并非严格可交换，如(Int, Bool)不能直接当(Bool, Int)来用，但这两者携带的信息是一样的。可以看作是不同格式存储了相同数据的，用swap :: (a, b) -\u0026gt; (b, a)可给出同构关系。\n通过嵌套，可以得出((a, b), c) (a, (b, c))，这两种类型是不同的，但它们包含的元素是一一对应的，这也是同构。\n在同构的条件下，不再坚持严格的相等性的话，可以揭示unit类型()类似数字乘法中的1。类型a的值与一个unit值构成的元组，除了a值之外，什么也不包含，即：(a, ())，该类型与a是同构的。可以构造同构关系rho :: (a, ()) -\u0026gt; a rho_inv :: a -\u0026gt; (a, ())。\nThese observations can be formalized by saying that𝐒𝐞𝐭(the category of sets)is a monoidal category. It’s a category that’s also a monoid, in the sense that you can multiplyo bjects(here,take their Cartesian product).\n如果用形式化语言描述这些现象，可以说 Set（集合的范畴）是一个幺半群范畴，亦即这种范畴也是一个幺半群，这意味着你可以让对象相乘（在此，就是笛卡尔积）。\nHaskell中还有更通用的办法来构造积类型，data Pair a b = P a b，Pair a b 是由 a 与 b 参数化的类型的名字；P 是数据构造子的名字。因为类型构造子与数据构造子的命名空间是彼此独立的，因此二者可以同名：data Pair a b = Pair a b\nRecord - 记录 可以给字段进行命名，这种构造称为记录，\n如data Element = Element { name :: String , symbol :: String , atomicNumber :: Int }\nSum Types - 和类型 就像集合的范畴中的积能产生积类型那样，余积能产生和类型。\nHaskell 官方实现的和类型如下：data Either a b = Left a | Right b。Either 在同构意义下是可交换的，也是可嵌套的，而且在同构意义下，嵌套的顺序不重要。因此，我们可以定义与三元组相等的和类型：data OneOfThree a b c = Sinistral a | Medial b | Dextral c\n可以证明 Set 对于余积而言也是个（对称的）幺半群范畴。二元运算由不相交和（Disjoint Sum）来承担，unit 元素由初始对象来扮演。用类型术语来说，我们可以将 Either 作为幺半群运算符，将 Void 作为中立元素。也就是说，可以认为 Either 类似于运算符 +，而 Void 类似于 0。这与事实是相符的，将 Void 加到和类型上，不会对和类型有任何影响。例如：Either a Void 与 a 同构。这是因为无法为这种类型构造 Right 版本的值——不存在类型为 Void 的值。Either a Void 只能通过 Left 构造子产生值，这个值只是简单的封装了一个类型为 a 的值。这就类似于 a + 0 = a。\nHaskell 中有Maybe类型data Maybe a = Nothing | Just a，表示值可能不存在。Maybe是两个类型的和。将两个构造子分开看，观察第一个，data NothingType = Nothing，它是一个叫做 Nothing 的单值的枚举。换句话说，它是一个单例，与 unit 类型 () 等价；第二个 data JustType a = Just a 的作用就是封装类型 a。于是 data Maybe a = Either () a\nHaskell 中的列表类型，它被定义为一个（递归）的和类型：data List = Nil | Cons a (List a)\nAlgebra of Type - 类型代数 积类型与和类型，单独使用其中一个就可以定义一些有用的数据结构，但是二者组合起来或更加强大。\n已经见识了类型系统中两种可交换的幺半群结构：用 Void 作为中性元素的和类型，用 () 作为中性元素的积类型。可以将这两种类型比喻为加法与乘法。在这个比喻中，Void 相当于 0，而 () 相当于 1。\n现在来思考如何增强这个比喻。例如，与 0 相乘的结果为 0 么？换句话说，一个积类型中的一个成员是 Void，那么这个积类型与 Void 同构么？例如，是否存在一个 Int 与 Void 构成的序对？要创建序对，需要两个值。Int 值很容易获得，但是 Void 却没有值。因此，对于任意类型 a 而言，(a, Void) 是不存在的——它没有值——因此它等价于 Void。换句话说，a * 0 = 0。\n另外加法与乘法在一起的时候，存在着分配律：a * (b + c) = a * b + a * c 那么对于积类型与和类型而言，是否也存在分配律？是的，不过一般是在同构意义下存在。上式的左半部分相当于：(a, Either b c) 右半部分相当于：Either (a, b) (a, c)，现给出能证明它们同构的转换\n1 2 3 4 5 6 7 8 9 10 11 prodToSum :: (a, Either b c) -\u0026gt; Either (a, b) (a, c) prodToSum (x, e) = case e of Left y -\u0026gt; Left (x, y) Right z -\u0026gt; Right (x, z) sumToProd :: Either (a, b) (a, c) -\u0026gt; (a, Either b c) sumToProd e = case e of Left (x, y) -\u0026gt; (x, Left y) Right (x, z) -\u0026gt; (x, Right z) Mathematicians have a name for two such intertwined monoids: it’s called a semiring. It’s not a full ring, because we can’t define subtraction of types. That’s why a semiring is sometimes called a rig, which is a pun on “ring without an n” (negative).\n数学家们为这种相互纠缠的幺半群取了个名字：半环（Semiring）。之所以不叫它们全环，是因为我们无法定义类型的减法。这就是为何半环有时也被称为 rig 的原因，因为『Ring without an n（negative，负数）』。在此不关心这些问题，我们关心的是怎么描述自然数运算与类型运算之间的对应关系。现列出一些对应关系\nNumbers Types 0 Void 1 () a + b `Either a b = Left a a * b (a, b) or Pair a b = Pair a b 2 = 1 + 1 `data Bool = True 1 + a `data Maybe = Nothing 列表类型相当有趣，因为它被定义为一个方程的解，因为我们要定义的类型出现在方程两侧： List a = Nil | Cons a (List a) 如果将 List a 换成 x，就可以得到这样的方程： x = 1 + a * x 不能使用传统的代数方法去求解这个方程，因为对于类型，我们没有相应的减法与除法运算。不过，可以用一系列的替换，即不断的用 (1 + a*x) 来替换方程右侧的 x，并使用分配律，这样就有了下面的结果：\n1 2 3 4 5 x = 1 + a*x x = 1 + a*(1 + a*x) = 1 + a + a*a*x x = 1 + a + a*a*(1 + a*x) = 1 + a + a*a + a*a*a*x ... x = 1 + a + a*a + a*a*a + a*a*a*a... 最终是一个积（元组）的无限和，这个结果可被解释为：一个列表，要么是空的，即 1；要么是一个单例 a；要么是一个序对 a*a；要么是一个三元组 a*a*a；……以此类推\n用符号变量来解方程，这就是代数！因此上面出现的这些数据类型被称为：代数数据类型。\n类型代数的一个非常重要的解释。注意，类型 a 与类型 b 的积必须包含类型 a 的值与类型 b 的值，这意味着这两种类型都是有值的；两种类型的和则要么包含类型 a 的值，要么包含类型 b 的值，因此只要二者有一个有值即可。逻辑运算 and 与 or 也能形成半环，它们也能映射到类型理论：\nLogic Types false Void true () `a a \u0026amp;\u0026amp; b (a, b) 这是更深刻的类比，也是逻辑与类型理论之间的 Curry-Howard 同构的基础。以后在讨论函数类型时，对此再作探讨。\n","date":"2020-03-20T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/6.simple-algebraic-data-type/","title":"1.6 Simple Algebraic Data Types - 简单代数类型"},{"content":"Products and Coproducts Link to Blog\n\u0026lt;译\u0026gt; 积与余积\nThere is a common construction in category theory called the universal construction for defining objects in terms of their relationships. One way of doing this is to pick a pattern, a particular shape constructed from objects and morphisms, and look for all its occurrences in the category.\n范畴论中有一个常见的构造，叫做泛构造（Universal Construction），它就是通过对象之间的关系来定义对象，其方式之一就是拮取一个模式——由对象与态射构成的一种特殊的形状，然后在范畴中观察它的各个方面。\nInitial Object - 初始对象 把范畴想象成一张网，箭头从范畴的一端流向另一端。有序范畴中会出现这种现象，如偏序范畴。尝试将这一概念推广\nWe could generalize that notion of object precedence by saying that object 𝑎 is “more initial” than object 𝑏, if there is an arrow (amorphism) going from 𝑎 to 𝑏. We would then define the initial object as one that has arrows going to all other objects.\n如果要说对象a比对象b更“初始”，那么必定存在一个箭头/态射是从a到b的。如果有个对象，它发出的所有箭头指向所有其它对象，那么这个对象就叫做初始对象。\n对于一个给定范畴，可能无法保证初始对象一定存在，这还好说，但更麻烦的问题是，可能存在多个初始对象。考虑有序范畴，有序范畴要求任意两个对象之间最多存在一个箭头：小于或等于。基于此，给出定义：\nThe initial object is the object that has one and only one morphism going to any object in the category.\n初始对象：这种对象有且仅有一个态射指向范畴中的任意一个对象\n然而，即使这样定义初始对象也无法担它的唯一性（如果它存在），但是这个定义能担保最好的一件事是：在同构意义下，它具有唯一性。同构（Isomorphism）在范畴论中非常重要，过会儿再谈它。现在，我们只需要认定，初始对象的定义主要是为了让初始对象在同构意义下具备唯一性。\n在此，给出几个与初始对象有关的例子：偏序集（通常称为 poset）的初始对象是那个最小的对象。有些偏序集没有初始对象——例如整数集。\n集合范畴中，初始对象是空集。在Haskell总，空集相当于Void类型（C++中没有对应类型），而且从Void到任意类型的多态函数是唯一的，叫做absurd（absurd :: Void -\u0026gt; a）。这是一个态射族，由于它的存在，Void才成为类型范畴中的初始对象。\nTerminal Object - 终端对象 The terminal object is the object with one and only one morphism coming to it from any object in the category.\n终端对象，这种对象有且仅有一个态射来自范畴中的任意对象。\n同样，终端对象在同构意义下具有唯一性。在偏序集内，如果存在着终端对象，那么它是那个最大的对象。在集合范畴中，终端对象是一个单例。我们已经讨论过单例，它们相当于 C++ 中的 void 类型，也相当于 Haskell 中的 unit 类型 ()。单例就是只有一个值的类型，在 C++ 中是隐式的，在 Haskell 中是显式的。已知，有且仅有一个纯函数，从任意类型到unit类型 unit :: a -\u0026gt; ()。因此，对于单例而言，终端对象的所有条件都是能够满足的。\n注意，以上例子中，唯一性是非常重要的，因为有些集合（实际上是除了空集之外的所有集合）会有来自其它集合的态射。例如，有这样一个结果为bool类型的函数/谓词，yes :: a -\u0026gt; bool，但是bool不是一个终结对象，因为至少还存在另一个no :: a -\u0026gt; bool。坚持唯一性能够得到足够的精度，可以将终端对象的定义紧缩为一种类型。\nDuality - 对偶 注意到初始对象和终结对象是对称的，二者之间唯一的区别的是态射的方向。事实上，对于任意范畴C，总能定义一个相反范畴 C' $C^{op}$，只需要将C中的箭头反转一下，然后重新定义一下态射的复合方式，相反的范畴能自然满足所有的范畴法则。例如，在C中，f :: a -\u0026gt; b，g :: b -\u0026gt; c，h :: a -\u0026gt; c = g ∘ f，那么逆向为f' :: b -\u0026gt; a，g' :: c -\u0026gt; b，h' :: c -\u0026gt; a = f' ∘ g'。恒等箭头保持不变（或者说反转后跟原本一样）。\n对偶是范畴的一个非常重要的性质。提出的每一种构造，都有其对偶的构造；证明的每一种定义，都能免费获得一个“对偶”版。相反的范畴中的概念通常以“余”(\u0026ldquo;co\u0026rdquo;)开头，因此有了积/product和余积/coproduct，单子/monad和余单子/comonad，锥/cones和余锥/cocones等。不存在什么余余单子/cocomonad，因为反转两次之后又会变成原样。\n一个范畴中的终端对象，在其相反范畴中就是初始对象。\nIsomorphisms - 同构 定义相等是很难的，不管是从程序的角度来定义两个对象相等，还是从数学上定义两个东西相等。数学家也描述不了相等的意义，所以定义了很多种侧重不同视角的相等：命题相等、内涵相等、外延相等，还有拓扑类型理论中的路径相等之类。于是出现了一个弱化的概念-同构/isomorphism\n直觉上，同构对象看上去是一样的，它们有着相同的形状。这意味着每个对象的每个部分与另一对象的某个部分形成一对一的映射，我们的“仪器”检测出它们是彼此的拷贝。在数学上，这意味着存在一个从a到b的映射，也存在一个从b到a的映射，这两个映射是互逆的。在范畴论中，我们用态射取代了映射。一个同构/isomorphism是一个可逆的态射/morphism；或者一对互逆的态射/morphism。\n可以通过复合与恒等来理解互逆。若态射f和态射g的复合结果是恒等态射，那么g是f的逆。这体现为一下两方程，因为两态射存在两种复合形式\n1 2 3 4 5 f :: a -\u0026gt; b g :: b -\u0026gt; a g . f = ida f . g = idb 前面提到，初始/终结对象在同构意义下具有唯一性，意思就是两个初始/终结对象是同构的。考虑上面代码展示的范畴，复合g . f必定是个从a到a的态射。但是a是个初始，所以只能有一个从a到a的态射。由于是在一个范畴中，所以知道这个唯一的从a到a的态射就是恒等态射。因此，g . f等于恒等态射；同理 f . g也是恒等态射。这就证明了f和g是互逆的，因此两个初始对象就是同构的。\n注意，上述证明中，我们使用的是从初始对象到它本身的态射的唯一性。也需要f和g也是唯一的，因为初始对象不仅在同构意义下具有唯一性，而且这个同构是唯一的。理论上，两个对象之间可能存在不止一种同构关系。这种“在同构意义下具有唯一性，且这个同构是唯一的”是所有泛构造的一个重要性质\nProduct - 积 还有一个泛构造，叫做积/Product。从积到每个成分，存在两个投影。在Haskell中，这两个函数被称为fst和snd，它们分别从元组/序对中获取第一成员和第二成员，fst :: (a, b) -\u0026gt; a和snd :: (a, b) -\u0026gt; b。\n借助这些有限的前提，我们尝试定义集合范畴中对象和态射的模式，这种模式可以引导我们去构造两个集合a与b的积。这个模式由对象c和两个态射p和q构成，p与q将c分别连想a和b，p :: c -\u0026gt; a和q :: c -\u0026gt; b。所有符合这个模式的c都认为是候选积，这样的c可能有很多。\n举个例子，从Haskell类型中选择Int Bool，让它们相乘，并将 Int Bool作为候选积。假设 Int是候选积，Int 能够被认为是 Int 与 Bool 相乘的候选积吗？是的，它能，因为它具有以下投影：p :: Int -\u0026gt; Int = \\x -\u0026gt; x q :: Int -\u0026gt; Bool = _ -\u0026gt; True，它符合候选积的条件。\n还有一个(Int, Int, Bool)的三元组，它也是个合法的候选积，因为存在p :: (Int, Int, Bool) -\u0026gt; Int = (x, _, _) -\u0026gt; x q :: (Int, Int, Bool) -\u0026gt; Bool = (_, _, b) -\u0026gt; b。注意到，第一个候选积太小了，它只覆盖了积的Int维度，第二个太大了，存在一个重复的Int维度。\n我们还没有探索这个泛构造的其它部分：等级划分。我们希望比较这种模式的两个实例，也就是说对于候选积c与c\u0026rsquo;，想要进行一些比较，以便做出c比c\u0026rsquo;更好的结论。如果有个c\u0026rsquo;到c的态射m，虽然可以基于这个态射认为 c 比 c\u0026rsquo; 更好，但是这样还是太弱了。我们还希望c的p和q比c\u0026rsquo;的p\u0026rsquo;和q\u0026rsquo;更好，这意味着可以通过态射m从p和q分别构造出p\u0026rsquo;和q\u0026rsquo; p' :: c' -\u0026gt; a = p . m q' :: c' -\u0026gt; b = q . m。从某个角度看，有点像是m是一个公因子的感觉。\n基于前面建立的直觉，现在看一下(Int, Bool)及其fst snd为何比之前我们给出的两个候选积更好\n对于第一个候选积，m :: Int -\u0026gt; (Int, Bool) = x -\u0026gt; (x, True)，这个候选积的两个投影p和q可重构为p x = fst (m x) = x q = snd (m x) = True\n对于第二个候选积，m :: (Int, Int, Bool) = (x, _, b) -\u0026gt; (x, b)\n现在给出理由，为何说(Int, Bool)比前两个候选积更好，先尝试找一个m\u0026rsquo;尝试构造出p和q\n对第一个例子 fst = p . m' snd = q . m' 已知q总是返回True的，但是元组自己是可以有False值的，那么构造的这个 snd 是不对的，无法构造 snd\n对第二个例子，能够在 p 或 q 运行后保留足够的信息，但是对于 fst 与 snd 而言，它们存在着多种因式化方式，因为 p 与 q 会忽略三元组的第 2 个元素，这就意味着我们的 m\u0026rsquo; 可以在第 2 个元素的位置放任意东西，例如：m' (x, b) = (x, x, b) 或者 m' (x, b) = (x, 42, b) 等。\n总之，对任何给定的类型c和投影p q，存在着唯一的一个 m 可将 c 映射成笛卡尔积 (a, b)。m :: c -\u0026gt; (a, b) = x -\u0026gt; (p x, q x)。这样就决定了笛卡尔积 (a, b) 是最好的候选积，这意味着这种泛构造对于集合范畴是有效的，它涵盖了任意两个集合的积。\n现在，我们忘记集合，使用相同的泛构造来定义任意范畴中两个对象的积。这样的积并非总是存在，但是一旦它存在，它就在同构意义下具有唯一性，而且这个同构是唯一的。\nA product of two objects 𝑎 and 𝑏 is the object 𝑐 equipped with two projections such that for any other object 𝑐′ equipped with two projections there is a unique morphism 𝑚 from 𝑐′to 𝑐 that factorizes those projections.\n对象 a 与对象 b 的积是伴随两个投影的对象 c。对于任何其他伴随两个投影的对象 c\u0026rsquo; 而言，存在唯一的从 c\u0026rsquo; 到 c 的态射，这个态射可以因式化这两个投影。\n一个高阶函数能够生成因子 m，这个高阶函数有时被称为因子生成器。对于本文的示例中，它是这样的函数：\n1 2 factorizer :: (c -\u0026gt; a) -\u0026gt; (c -\u0026gt; b) -\u0026gt; (c -\u0026gt; (a, b)) factorizer p q = \\x -\u0026gt; (p x, q x) Coproduct - 余积 同范畴论中每个构造一样，积有一个对偶，叫做余积。将积的范式中的箭头反转，就可以得到一个对象 c，它伴随两个入射 i 与 j——从 a 到 c 的态射与从 b 到 c 的态射。i :: a -\u0026gt; c j :: b -\u0026gt; c。等级也反转了，c比c\u0026rsquo;更好的条件是：存在从c到c\u0026rsquo;的态射m，可以“因式化”入射。i' = m . i j' = m . j\n这个“最好”的对象就是，具有唯一的态射从其本身指向其它对象，这种对象就叫余积，并且如果它存在，那么它就在同构意义下具有唯一性，而且这个同构是唯一的。\nA coproduct of two objects 𝑎 and 𝑏 is the object 𝑐 equipped with two injections such that for any other object 𝑐′ equipped with two injections there is a unique morphism 𝑚 from 𝑐 to 𝑐′ that factorizes those injections.\n两个对象 a 与 b 的余积是对象 c，当且仅当 c 伴随着两个入射，而且任何一个其他的伴随两个入射的对象 c\u0026rsquo;，只存在唯一的从 c 到 c\u0026rsquo; 的态射 m，并且 m 可以因式化这些入射。\n在集合的范畴中，余积就是两个集合的不相交求并运算。集合 a 与集合 b 的不相交求并结果中的一个元素，要么是 a 中的元素，要么是 b 中的元素。\nHaskell中内置的积是用元组实现，内置的余积是Either数据类型实现Either a b = Left a | Right b。\n程序中的余积可以这样定义data Contact = PhoneNum Int | EmailAddr String / type Contact = PhoneNum Int | EmailAddr String\n正如我们刚才所定义的积的因式生成器一样，我们也可以为余积定义一个。对于给定的候选余积 c 以及两个候选入射 i 与 j，为 Either 生成因式函数的的因式生成器可定义为：\n1 2 3 factorizer :: (a -\u0026gt; c) -\u0026gt; (b -\u0026gt; c) -\u0026gt; Either a b -\u0026gt; c factorizer i j (Left a) = i a factorizer i j (Right b) = j b Asymmetry - 非对称 集合的范畴不会随箭头的反转而出现对称性。\nNotice that while the empty set has a unique morphism to any set (the absurd function),it has no morphisms coming back. The singleton set has a unique morphism coming to it from any set, but it also has out going morphisms to every set (except for the empty one).\n空集可以向任意一个集合发出唯一的态射（absurd 函数），但是它没有其他集合发来的态射。单例集合拥有任意集合发来的唯一的态射，但它也能向任一集合（除了空集）发出态射。由终端对象发出的态射在拮取其他集合中的元素方面扮演了重要的角色（空集没有元素，因此没什么东西可拮取的）。\n函数是建立在它的定义域（Domain）上的（在编程中，称之为全函数），它不必覆盖余定义域（Codomain，译注：可能叫陪域更正式一些）。我们已经看到了一些极端的例子（实际上，所有定义域是空集的函数都是极端的）：定义域是单例的函数，意味着它只在余定义域上选择了一个元素。若定义域的尺度远小于余域的尺度，我们通常认为这样的函数是将定义域嵌入余定义域中了。例如，我们可以认为，定义域是单例的函数，它将单例嵌入到了余定义域中。我将这样的函数称为嵌入函数，但是数学家给从相反的角度进行命名：覆盖了余定义域的函数称为满射（Surjective）函数或映成（Onto）函数。\n函数的非对称性也表现为，函数可以将定义域中的许多元素映射为余定义域上的一个元素，也就是说函数坍缩了。一个极端的例子是函数使整个集合坍缩为一个单例，unit 函数干的就是这事。这种坍缩只能通过函数的复合进行混成。两个坍缩函数的复合，其坍缩能力要强过二者单兵作战。数学家为非坍缩函数取了个名字：内射（Injective）或一对一（One-to-one）映射。\n当然，有许多函数即不是嵌入的，也不是坍缩的。它们被称为双射（Bijection）函数，它们是完全对称的，因为它们是可逆的。在集合范畴中，同构就是双射的。\n","date":"2020-03-19T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/5.products-and-coproducts/","title":"1.5 Products and Coproducts - 积与余积"},{"content":"Kleisli Categories - Kleisli范畴 可以基于范畴论对副作用/非纯函数进行建模。\n考虑一个跟踪程序，在命令式编程语言中，经常是以修改某些全局状态来实现，例如\n1 2 3 4 5 string logger; bool negate(bool b) { logger += \u0026#34;Not so!\u0026#34;; return !b; } 通过修改全局变量logger，实现对于程序运行情况的跟踪。但是现代编程语言中，一般尽可能不去修改全局状态，尤其是并发编程这一复杂情景。我们可以考虑重构以上方法，使之变成一个纯函数\n1 2 3 pair\u0026lt;bool, string\u0026gt; negate(bool b, string logger) { return make_pair(!b, logger + \u0026#34;Not so! \u0026#34;); } 现在这是一个纯函数了，它没有副作用了，传入相同的参数即可得到完全相同的结果。但若是关心跟踪的详细情况，考虑到累积性，不得不收集这个函数运行情况的全部历史，每调用它一次，就产生一个结果。作为一个库函数，这是特别难用的。调用者可以随意丢掉返回值当中的字符串（若是他不关心跟踪的具体情况），可是还必须传入一个字符串参数，这用起来很不方便了。\n考虑消除这些东西，将我们关心的东西分离出来。本例中，negate主要任务是将布尔值转换成另一个，至于记录运行情况，那是次要的。尽管日志信息对于这个函数而言是特定的，但是将信息汇集到一个连续的日志这一任务是可单独考虑的。我们依然想让这个函数生成日志信息，但是可以减轻一下它的负担。现在有一个折中的解决方案\n1 2 3 pair\u0026lt;bool, string\u0026gt; negate(bool b) { return make_pair(!b, \u0026#34;Not so! \u0026#34;); } 这样，日志信息的汇集工作就被转移至函数的当前调用之后且在下一次被调用之前的时机。\n现在给出一个更现实的示例，现有一个小写转大写（字符串到字符串）和字符串分割成单词（字符串到字符串向量）的函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 string toUpper(string s) { string result; int (*toupperp)(int) = \u0026amp;toupper; // toupper is overloaded transform(begin(s), end(s), back_inserter(result), toupperp); return result; } vector\u0026lt;string\u0026gt; toWords(string s) { return words(s); } vector\u0026lt;string\u0026gt; words(string s) { vector\u0026lt;string\u0026gt; result{\u0026#34;\u0026#34;}; for (auto i = begin(s); i != end(s); ++i) { if (isspace(*i)) result.push_back(\u0026#34;\u0026#34;); else result.back() += *i; } return result; } 现在我们想将函数toUpper和toWords修改一下，让它们返回值肩负日志信息。\n通过“修饰”两个函数的返回值，采用更泛化一些的方式，定义一个Writer模板先\n1 2 template\u0026lt;class A\u0026gt; using Writer = pair\u0026lt;A, string\u0026gt;; 然后修改两个函数\n1 2 3 4 5 6 7 8 9 10 Writer\u0026lt;string\u0026gt; toUpper(string s) { string result; int (*toupperp)(int) = \u0026amp;toupper; transform(begin(s), end(s), back_inserter(result), toupperp); return make_pair(result, \u0026#34;toUpper \u0026#34;); } Writer\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; toWords(string s) { return make_pair(words(s), \u0026#34;toWords \u0026#34;); } 现在想将这两函数复合，得到一个先将字符串转大写，然后将之分割成单词的函数，此外也应当记下执行跟踪信息也不能丢失，所以这样实现\n1 2 3 4 5 Writer\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; process(string s) { auto p1 = toUpper(s); auto p2 = toWords(p1.first); return make_pair(p2.first, p1.second + p2.second); } 现在已经完成了目标：日志的汇集不再由单个的函数来操心。这些函数各自产生各自的消息，然后在外部汇总为一个更大的日志。\n整个程序都这么写的话，那么会有特别多重复代码，解决方式便是进行抽象。但这不是普通的抽象，是在对函数的复合本身进行抽象。由于复合是范畴论的本质，因此在动手之前，我们先从范畴的角度分析一下这个问题。\nThe Writer Category 对那几个函数的返回类型进行“修饰”，其意图是为了让返回类型肩负着一些有用的附加功能。这一策略相当有用，下面将给出更多的示例。起点还是常规的的类型与函数的范畴。我们将类型作为对象，与以前有所不同的是，现在将装帧过的函数作为态射了。\n假设我们要修饰从int到bool的isEven函数，然后将修饰后的函数作为态射，尽管修饰后的函数返回一个元组/序对，但我们依然认为它是个从int到bool的态射。\n1 2 3 pair\u0026lt;bool, string\u0026gt; isEven(int n) { return make_pair(n % 2 == 0, \u0026#34;isEven \u0026#34;); } 按照范畴中态射的复合法则，应当可以与我们之前定义的negate进行复合。\n1 2 3 pair\u0026lt;bool, string\u0026gt; negate(bool b) { return make_pair(!b, \u0026#34;Not so! \u0026#34;); } 显然无法通过常规方式进行态射复合，因为输入输出不匹配，我们可以这样实现\n1 2 3 4 5 pair\u0026lt;bool, string\u0026gt; isOdd(int n) { pair\u0026lt;bool, string\u0026gt; p1 = isEven(n); pair\u0026lt;bool, string\u0026gt; p2 = negate(p1.first); return make_pair(p2.first, p1.second + p2.second); } 对这种新范畴中态射复合的法则进行总结：\n执行与第一个态射所对应的装帧函数，得到第一个序对； 从第一个序对中取出第一个元素，将这个元素传给与第二态射对应的装帧函数，得到第二个序对； 将两个序对中的第二个元素（字符串）连接起来； 将计算结果与连接好的字符串捆绑起来作为序对返回。 若想将这种复合抽象为 C++ 中的高阶函数，必须根据与我们的范畴中的三个对象相对应的三种类型构造一个参数化模板。这个函数应该接受能遵守上述复合法则的两个可复合的装帧函数，返回第三个装帧函数：\n1 2 3 4 5 6 7 8 9 10 template\u0026lt;class A, class B, class C\u0026gt; function\u0026lt;Writer\u0026lt;C\u0026gt;(A)\u0026gt; compose(function\u0026lt;Writer\u0026lt;B\u0026gt;(A)\u0026gt; m1, function\u0026lt;Writer\u0026lt;C\u0026gt;(B)\u0026gt; m2) { return [m1, m2](A x) { auto p1 = m1(x); auto p2 = m2(p1.first); return make_pair(p2.first, p1.second + p2.second); }; } 使用这个函数去复合之前的toUpper和toWords\n1 2 3 Writer\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; process(string s) { return compose\u0026lt;string, string, vector\u0026lt;string\u0026gt;\u0026gt;(toUpper, toWords)(s); } 对于支持 C++14 的编译器，它支持具有返回类型推导功能的泛型匿名函数\n1 2 3 4 5 6 7 auto const compose = [](auto m1, auto m2) { return [m1, m2](auto x) { auto p1 = m1(x); auto p2 = m2(p1.first); return make_pair(p2.first, p1.second + p2.second); }; }; 用这个新的compose可以简化process\n1 2 3 Writer\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; process(string s){ return compose(toUpper, toWords)(s); } 在这个新范畴里，我们已经定义了态射复合，但是还没有定义恒等态射。这些恒等态射肯定不是常规意义上的恒等态射！它们必须是一个从（装帧之前的）类型 A 到（装帧之后的）类型 A 的的态射，即：\n1 Writer\u0026lt;A\u0026gt; identity(A); 对于复合而言，它们的行为必须像 unit。若要符合上面的态射复合的定义，那么这些恒等态射不应该修改传给它的参数，并且对于日志它们仅贡献一个空的字符串：\n1 2 3 4 template\u0026lt;class A\u0026gt; Writer\u0026lt;A\u0026gt; identity(A x) { return make_pair(x, \u0026#34;\u0026#34;); } 我们所定义的这个范畴是一个合法的范畴。特别是，我们所定义的态射的复合是遵守结合律的，虽然这无关紧要。如果你只关心每个序对的第一个元素，这种复合就是常规的函数复合。第二个元素会被连接起来，而字符串的连接也是遵守结合律的。\n这种构造适用于任何幺半群，而不仅仅是字符串幺半群。我们可以在 compose 中使用 mappend，在 identify 中使用 mempty。\nWriter in Haskell 同样的事情，在Haskell中做起来简单一些，而且也能得到编译器的很多辅助\n首先定义Writer\n1 type Writer a = (a, String) 声明复合操作符如下所示\n1 (\u0026gt;=\u0026gt;) :: (a -\u0026gt; Writer b) -\u0026gt; (b -\u0026gt; Writer c) -\u0026gt; (a -\u0026gt; Writer c) 其定义如下所示\n1 2 3 4 m1 \u0026gt;=\u0026gt; m2 = \\x -\u0026gt; let (y, s1) = m1 x (z, s2) = m2 y in (z, s1 ++ s2) 1 2 3 4 let ( \u0026gt;=\u0026gt; ) m1 m2 = fun x -\u0026gt; let (y, s1) = m1 x let (z, s2) = m2 y (z, s1 + s2) 恒等态射\n1 2 return :: a -\u0026gt; Writer a return x = (x, \u0026#34;\u0026#34;) 上面几个例子，用Haskell演示其实现如下所示\n1 2 3 4 toUpper :: String -\u0026gt; Writer String toWords :: String -\u0026gt; Writer [String] process = toUpper \u0026gt;=\u0026gt; toWords Kleisli Categories Link to Blog\n\u0026lt;译\u0026gt; Kleisli 范畴\n以上示例就是在演示Kleisli范畴，这是个建立在单子（Monad）之上的范畴，此处不讨论单子，只是在演示单子能干什么。在编程中，可以这样理解，在Kleisli范畴，从A到B的态射是从A到B的派生（“装饰”后的B）的函数。每个Kleisli范畴都定义了自己的态射复合运算以及支持这种复合运算的恒等态射。（“装饰”/“装帧”是个不严谨的说法，它相当于范畴论中的自函子 endofunctor）。\n以上示例中演示的是Writer单子（Writer Monad），专门用于跟踪函数执行情况。它也是纯计算过程中嵌入副作用这种一般性机制一个范例。\n可以将编程语言的类型与函数构建为集合的范畴（忽略底的存在）。在本文中，我们将这个模型扩展为一个稍微有些不同的范畴，其态射是经过装帧的函数，态射的复合所做的工作不仅仅是将一个函数的输出作为另一个函数的输入，它做了更多的事。这样，我们就多了一个可以摆弄的自由度：这种复合本身。对于传统上使用命令式语言并且通过副作用实现的程序，这种复合运算能够给出简单的指称语义。\nChallenge 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type \u0026#39;a CanBeFailed = Success of \u0026#39;a | Failed let idc x = Success x // \u0026gt;=\u0026gt; : (\u0026#39;a -\u0026gt; CanBeFailed\u0026lt;\u0026#39;b\u0026gt;) -\u0026gt; (\u0026#39;b -\u0026gt; CanBeFailed\u0026lt;\u0026#39;c\u0026gt;) -\u0026gt; (\u0026#39;a -\u0026gt; CanBeFailed\u0026lt;\u0026#39;c\u0026gt;) let ( \u0026gt;=\u0026gt; ) f1 f2 = fun x -\u0026gt; match f1 x with | Failed -\u0026gt; Failed | Success y -\u0026gt; match f2 y with | Failed -\u0026gt; Failed | Success z -\u0026gt; Success z let safeReciprocal x = match x with | 0.0 -\u0026gt; Failed | _ -\u0026gt; Success (1.0/x) let safeRoot x = if x \u0026gt;= 0.0 then Success (sqrt x) else Failed let safeRootReciprocal = safeRoot \u0026gt;=\u0026gt; safeReciprocal ","date":"2020-03-18T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/4.kleisli-categories/","title":"1.4 Kleisli Categories - Kleisli范畴"},{"content":"Categories Great and Small - 范畴可大可小 Categories Great and Small\n\u0026lt;译\u0026gt; 范畴，可大可小\nNo Objects - 无对象 The most trivial category is one with zero objects and, consequently, zero morphisms\n存在这样一个零对象零态射的范畴。它在包含所有范畴的范畴中。这是个合理的概念，既然空集合是合理的，为何不会有空范畴呢？\nSimple Graphs - 简单画图 可以仅通过箭头连接对象来构造出范畴。\n这样想象，基于任何一个有向图，仅通过添加更多箭头即可让它变成一个范畴。首先给每个节点添加一个恒等箭头（恒等态射 identity arrow），然后为那些“可复合”箭头添加一个新箭头作为它俩的复合，注意添加新箭头之后可能出现新的“可复合”箭头。\n重新审视这个过程，这便是在构造一个范畴。\n这种由一个给定图构造的范畴称为“自由范畴”。\nOrders - 序 考虑这样的范畴：它的态射表示的是两对象的关系，例如小于等于关系。我们考察一下这实质上是不是个范畴。问题1：有没有恒等态射？每个对象都小于等于它自己，OK！问题2：有没有复合关系？若a小于等于b，b小于等于c，则a小于等于c，OK！问题3：复合满足结合律吗？满足结合律！伴随这种关系的集合，叫做前序集（preorder），前序集是范畴\n可以加强这一关系，加一个附加条件，若有a小于等于b及b小于等于a可得a必定与b相等，这种叫做偏序集（partial order）\n若集合中任意两对象之间有这个偏序关系，那么得到了一个全序集（linear order/total order）\n把这些集合描绘为范畴，对于前序集而言，两对象之间最多只有一个态射，这种范畴也叫做瘦范畴(thin category)\n范畴C中，从对象a到对象b的态射的集合称为Hom-集(hom-set)，写作C(a,b) / $C(a,b)$ / $Hom_C(a,b)$。所以每个前序集的Hom-集要么是空的要么是单例的，（包括Hom-集C(a,a)，在任意前序集中，必定只包含一个从a到a的恒等态射，此Hom-集必定是单例的）。前序集中可能包含环，但是偏序集中不能包含环。\n理解清楚前序集、偏序集、全序集很重要，因为排序需要它们。常见的排序算法，例如快排、冒泡、归并等等，只能在全序集上正确运行；偏序集可以用拓扑排序算法来处理。\nMonoid as Set - 幺半群作为集合 Monoid is an embarrassingly simple but amazingly powerful concept. It’s the concept behind basic arithmetics: Both addition and multiplication form a monoid.\n幺半群（Monoid）是一个相当简单但是功能强大的概念。它是基本算数幕后的概念：只要有加法或乘法运算就可以形成幺半群。编程中的幺半群无所不在，表现为字符串、列表、可以“折叠”的数据结构、并发编程中的future、函数式响应编程中的事件等等。\na monoid is defined as a set with a binary operation.\n幺半群被定义为“一个伴有二元运算的集合”。这个二元运算必须满足结合律，集合中包含着一个特殊的元素，对于这个二元运算，该元素的行为像一个返回其自身的 unit。\n例如：包含0的自然数伴随加法运算，这就形成了一个幺半群。结合律是指(a+b)+c=a+(b+c)；特殊的元素是0，0+a=a+0。（自然数加法满足交换律，但交换律并非幺半群定义所需。例如字符串有加法/连接运算，该运算不满足交换律，有特殊的“中立”元素空字符串，这依然是一个幺半群）\n在Haskell中可以为幺半群定义一个类型类（type class），该类型包含一个中立元素mempty和二元运算mappend\n1 2 3 class Monoid m where mempty :: m mappend :: m -\u0026gt; m -\u0026gt; m -- currying form 注意，在 Haskell 中，无法解释 mempty 与 mappend 的幺半群性质，也就是说 mempty 是个什么样的中立者，mappend 符合怎样的结合律。因为这是程序猿的责任，毕竟 Haskell 不能未卜先知。\nHaskell classes are not as intrusive as C++ classes. When you’re defining a new type, you don’t have to specify its class upfront. You are free to procrastinate and declare a given type to be an instance of some class much later.\nHaskell中定义一个新的类型时，不需要声明它所属的类。可以之后再声明一个给定的类型是某个类的实例。\n例如可以将String声明为一个幺半群，并为之提供mempty和mappend的实现。\n1 2 3 instance Monoid String where mempty = \u0026#34;\u0026#34; mappend = (++) Haskell中，任何中缀运算符，用括号围住，就转化成了一个接受两个参数的函数。（F#中也是如此）\n注：Haskell中允许函数相等。但是从概念上讲，mappend = (++)（函数相等）与mappend s1 s2 = (++) s1 s2（相同参数产生的值相等）是不同的。前者是Hask范畴（若忽略掉表示无休止计算的底类型，是Set）中的态射相等。这样的方程更简洁，而且常被泛化到其它范畴。后者被称为外延相等（extensional equality），说的是对于任意两个输入字符串，mappend和(++)值相等。由于函数的参数值有时也被称为点（情同：f在点x处的值），外延相等也被称为point-wise相等，未指定参数的函数相等，称为point-free相等。\n幺半群作为范畴 在范畴论中，我们尝试的事情是放弃集合和它们的元素，转而讨论对象和态射。转换一下视角，从范畴的角度来看作用与集合的移动或转移二元运算。\n例如，有一个将自然数加5的运算，它将0映射成5，将1映射成6等，这样是自然数集上定义了一个函数，此时我们有了一个函数和一个集合。通常，对任意数字n，都有个加n的函数，“n的adder”。这些“adder”如何复合呢？加5的函数和加7的函数复合起来，可以得到加12的函数。“adder”的复合等同与加法规则。那么我们可以用函数复合来代替加法运算。当然，也有一个面向中立元素0的“adder”，这加0不改变任何东西，它是自然数集上的恒等函数。即使不以传统的加法规则作为参照，照样能给出 adder 们的复合规则。注意，adder 们的复合是符合结合律的，因为函数的复合是符合结合律的，而且我们也有个加 0 的函数作为恒等函数。\n现在，忘掉我们正在处理自然数集，将它视为一个对象，它伴随这一堆态射（这些“adder”）。幺半群是一个单对象范畴。每个幺半群可以表述为有一个态射集的单对象范畴，态射集中的态射都遵守复合规则。\n字符串连接是个有趣的例子，因为我们可以选择定义左连接还是右连接。这两个态射是彼此镜像的。\nYou might ask the question whether every categorical monoid — a one-object category — defines a unique set-with-binary-operator monoid\n是否每个范畴化的幺半群都会定义一个唯一的的伴随二元运算的幺半群？事实上，我们总能从单对象范畴中抽出一个集合，这个集合是态射的集合（如前面的中的adder）。换句话说，对于只含单个对象m的范畴M，我们有一个Hom-集 M(m,m)，很容易在这个集合中定义一个二元运算：两元素相乘相当于两态射的复合。从M(m,m)中拿出两个元素f和g，它们的乘积相当与f∘g。复合总是存在的，因为这些态射的源对象和目标对象是同一个。这种乘法运算也遵循范畴论中的结合律。此外恒等态射也必定存在。因此，我们总是从幺半群范畴中复原出幺半群集合。无论从那个角度看，它们是同一个东西。\n对于数学家的挑剔而言，有点小 bug：态射不必形成集合。在范畴的世界里，有比集合更大的东西。一个范畴，其中任意两个对象之间的态射们形成一个集合，这样的范畴是局部小的。不过，因为承诺不要太数学，所以我会忽略这些细枝末节，在讲 Haskell 的『记录』语法时，再谈论它们。\n范畴论中大量的有趣现象都来自于：hom-集里的元素可被视为遵守复合法则的态射，也可被视为集合中的点。在此，M 中的态射的复合就会变成集合 M(m,m) 中的幺半群式的乘法运算。\n","date":"2020-03-17T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/3.categories-great-and-small/","title":"1.3 Categories Great and Small - 范畴可大可小"},{"content":"Category Theory for Programmers 《面向程序员的范畴论》是Bartosz Milewski写的一系列文章，向程序员介绍范畴论的一些概念，并以代码进行概念演示。\n该系列博客被整理成一个Github仓库，用于生成可下载PDF，并加入其它语言的代码样例。\n国内也有大神对系列博客的第一部分进行了翻译。\n这一博客系列整理为阅读笔记，期望能在读完之后对范畴论能有自己的理解。\n作者博客：Bartosz Milewski\u0026rsquo;s Programming Cafe\n作者的YouTube：Bartosz Milewski\n作者的讲解视频：Bartosz Milewski\u0026rsquo;s YouTube playlists (Category Theory videos)\nCTFP-PDF：hmemcpy/milewski-ctfp-pdf\n中文译者个人首页：garfileo - SegmentFault\n一些有趣的文章：\n单子，想弄不懂都很难\n阅读笔记 本部分阅读笔记是对于翻译版和原文的对照阅读进行记录，后期比较草率。翻译版本仅第一部分，已重新写了其他笔记，并对内容进行了理解和延申\nCategory: The Essence of Composition Types and Functions Categories Great and Small Kleisli Categories Products and Coproducts Simple Algebraic Data Type Functors Functoriality Function Type Natural Transformations 以上Markdown文件链接可能失效，可以查看系列Part-1和Part-2\nTable of Contents The Preface\nPart One Category: The Essence of Composition Types and Functions Categories Great and Small Kleisli Categories Products and Coproducts Simple Algebraic Data Types Functors Functoriality Function Types Natural Transformations Part Two Category Theory and Declarative Programming Limits and Colimits Free Monoids Representable Functors The Yoneda Lemma Yoneda Embedding Part Three It’s All About Morphisms Adjunctions Free/Forgetful Adjunctions Monads: Programmer’s Definition Monads and Effects Monads Categorically Comonads F-Algebras Algebras for Monads Ends and Coends Kan Extensions Enriched Categories Topoi Lawvere Theories Monads, Monoids, and Categories 中文翻译版 \u0026lt;译\u0026gt; 写给程序猿的范畴论 · 序\n第一部分 \u0026lt;译\u0026gt; 范畴：复合的本质 \u0026lt;译\u0026gt; 类型与函数 \u0026lt;译\u0026gt; 范畴，可大可小 \u0026lt;译\u0026gt; Kleisli 范畴 \u0026lt;译\u0026gt; 积与余积 \u0026lt;译\u0026gt; 简单的代数数据类型 \u0026lt;译\u0026gt; 函子 \u0026lt;译\u0026gt; 函子性 \u0026lt;译\u0026gt; 函数类型 \u0026lt;译\u0026gt; 自然变换 YouTube Videos there are 3 playlists.\nCategory Theory Category Theory 1.1: Motivation and Philosophy Category Theory 1.2: What is a category? Category Theory 2.1: Functions, epimorphisms Category Theory 2.2: Monomorphisms, simple types Category Theory 3.1: Examples of categories, orders, monoids Category Theory 3.2: Kleisli category Category Theory 4.1: Terminal and initial objects Category Theory 4.2: Products Category Theory 5.1: Coproducts, sum types Category Theory 5.2: Algebraic data types Category Theory 6.1: Functors Category Theory 6.2: Functors in programming Category Theory 7.1: Functoriality, bifunctors Category Theory 7.2: Monoidal Categories, Functoriality of ADTs, Profunctors Category Theory 8.1: Function objects, exponentials Category Theory 8.2: Type algebra, Curry-Howard-Lambek isomorphism Category Theory 9.1: Natural transformations Category Theory 9.2: bicategories Category Theory 10.1: Monads Category Theory 10.2: Monoid in the category of endofunctors Category Theory II Category Theory II 1.1: Declarative vs Imperative Approach Category Theory II 1.2: Limits Category Theory II 2.1: Limits, Higher order functors Category Theory II 2.2: Limits, Naturality Category Theory II 3.1: Examples of Limits and Colimits Category Theory II 3.2: Free Monoids Category Theory II 4.1: Representable Functors Category Theory II 4.2: The Yoneda Lemma Category Theory II 5.1: Yoneda Embedding Category Theory II 5.2: Adjunctions Category Theory II 6.1: Examples of Adjunctions Category Theory II 6.2: Free-Forgetful Adjunction, Monads from Adjunctions Category Theory II 7.1: Comonads Category Theory II 7.2: Comonads Categorically and Examples Category Theory II 8.1: F-Algebras, Lambek\u0026rsquo;s lemma Category Theory II 8.2: Catamorphisms and Anamorphisms Category Theory II 9.1: Lenses Category Theory II 9.2: Lenses categorically Category Theory III Category Theory III 1.1: Overview part 1 Category Theory III 1.2: Overview part 2 Category Theory III 2.1: String Diagrams part 1 Category Theory III 2.2, String Diagrams part 2 Category Theory III 3.1, Adjunctions and monads Category Theory III 3.2, Monad Algebras Category Theory III 4.1, Monad algebras part 2 Category Theory III 4.2, Monad algebras part 3 Category Theory III 5.1, Eilenberg Moore and Lawvere Category Theory III 5.2, Lawvere Theories Category Theory III 6.1, Profunctors Category Theory III 6.2, Ends Category Theory III 7.1, Natural transformations as ends Category Theory III 7.2, Coends Table of Contents in Detail The Preface\nPart One Category: The Essence of Composition Arrows as Functions Properties of COmposition Composition is the Essence of Programming Challenges Types and Functions Who Needs Types? Types Are About Composability What Are Types? Why Do We Need a Mathematical Model? Pure and Dirty Functions Examples of Types Challenges Categories Great and Small No Objects Simple Graphs Orders Monoid as Set Monoid as Category Challenges Kleisli Categories The Writer Category Writer in Haskell Kleisli Categories Challenges Products and Coproducts Initial Object Terminal Object Duality Isomorphisms Products Coproduct Asymmetry Challenges Bibliography Simple Algebraic Data Types Product Types Records Sum Types Algebra Types Challenges Functors Functors in Programming The Maybe Functor Equational Reasoning Optional Typeclasses Functor in C++ The List Functor The Reader Functor Functors as Containers Functor Composition Challenges Functoriality Bifuncotrs Product and Coproduct Bifunctors Functorial Algebraic Data Types Functors in C++ The Writer Functor Covariant and Contravariant Functors Profunctors The Hom-Functor Challenges Function Types Universal Construction Curring Exponentials Cartesian Closed Categories Exponentials and Algebraic Data Types Zeroth Power Powers of One First Power Exponentials of Sums Exponentials of Exponentials Exponentials over Products Curry-Howard Isomorphism Bibliography Natural Transformations Polymorphic Functions Beyond Naturality Functor Category 2-Categories Conclusion Challenges Part Two Declarative Programming Limits and Colimits Limit as Natural Isomorphism Examples of Limits Colimits Continuity Challenges Free Monoids Free Monoid in Haskell Free Monoid Universal Construction Challenges Representable Functors The Hom Functor Representable Functors Challenges Bibliography The Yoneda Lemma Yoneda in Haskell Co-Yoneda Challenges Bibliography Yoneda Embedding The Embedding Application to Haskell Preorder Example Naturality Challenges Part Three It\u0026rsquo;s All About Morphisms Functors Commuting Diagrams Natural Transformations Natural Isomorphisms Hom-Sets Hom-set Isomorphisms Asymmetry of Hom-Sets Challenges Adjunctions Adjunction and Unit/Counit Pair Adjunction and Hom-Sets Product from Adjunction Exponential from Adjunction Challenges Free/Forgetful Adjunctions Some Intuitions Challenges Monads: Programmer\u0026rsquo;s Definition The Klesli Category Fish Anatomy The do Notation Monads and Effects The Problem The Solution Partiality Nondeterminism Read-Only State Write-Only State State Exceptions Continuations Interactive Input Interactive Output Conclusion Monads Categorically Monoidal Categories Monoid in a Monoidal Category Monads as Monoids Monads from Adjunctions Comonads Programming with Comonads The Product Comonad Dissecting the Composition The Stream Comonad Comonad Categorically The Store Comonad Challenges F-Algebras Recursion Category of F-Algebras Natural Numbers Catamorphisms Folds Coalgebras Challenges Algebras for Monads T-algebras The Kleisli Category Coalgebras for Comonads Lenses Challenges Ends and Coends Dinatural Transformations Ends Ends as Equalizers Natural Transformations as Ends Coends Ninja Yoneda Lemma Profunctor Composition Kan Extensions Right Kan Extension Kan Extension as Adjunction Left Kan Extension Kan Extensions as Ends Kan Extensions in Haskell Free Functor Enriched Categories Why Monoidal Category? Monoidal Category Enriched Category Preorders Metric Spaces Enriched Functors Self Enrichment Relation to 2-Categories Topoi Subobject Classifier Topos Topoi and Logic Challenges Lawvere Theories Universal Algebra Lawvere Theories Models of Lawvere Theories The Theory of Monoids Lawvere Theories and Monads Monads as Coends Lawvere Theory of Side Effects Challenges Further Reading Monads, Monoids, and Categories Bicategories Monads Challenges Bibliography ","date":"2020-03-15T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/intro/","title":"范畴论笔记 - 序"},{"content":"Types and Functions - 类型与函数 Link to Blog\n\u0026lt;译\u0026gt; 类型与函数\nWho Need Types? 类型检查器可以基于类型信息进行对程序进行更深入的校验。\nTypes Are About Composability Category theory is about composing arrows. But not any two arrows can be composed. The target object of one arrow must be the same as the source object of the next arrow.\n编程实践中，两函数的输入和输出类型匹配，它们才能复合成功。当然，一般编程语言也提供了绕过类型检查的后门，例如强制类型转换、一些unsafe的库等。此外，现代编程语言的编译器也具备强大的类型推断能力，这样编写代码时可以省去一部分类型声明。\nWhat Are Types? The simplest intuition for types is that they are sets of values.\nSets can be finite or infinite.\n类型可以认为是一些值的集合，这个集合中的元素可以是有限的，也可能是无穷的。考虑Bool类型，这是个仅有两个元素True``False的集合；而String类型，这就是个无限元素的集合。\nThere are problems with polymorphic functions that involve circular definitions, and with the fact that you can’t have a set of all sets.\n多态函数存在循环定义问题，基于这一事实，无法获得一个所有集合的集合。\n集合的范畴名字就叫做“Set”，在“Set”中，对象是集合，态射（箭头）是函数。\n理想世界中，我们可以将Haskell类型视为集合，将函数视为集合间的数学函数。但这有一个问题，数学函数不执行任何代码，它只知道答案。一个Haskell函数必须执行计算然后得到答案。若是计算可以在有限步骤内完成，那么这没问题；但这个计算若是包含递归，可能永远不会结束。由于函数能否在有限步得出结果的不可预知性，所以我们也无法禁止这些无休止计算的函数。\nExtend every type by one more special value called bottom and denoted by _|_. This \u0026ldquo;value\u0026rdquo; corresponds to a non-terminating computation.\n引入了一个特殊的值，叫做“底”，Haskell中记作_|_，它是所有类型的子类型，它用于表示无休止计算。例如声明一个函数返回Bool，可能得到的结果是True``False``_|_，最后的值表示计算永远不会终结。\n可以将这个“底”接受为类型系统的一部分，那么可以所有运行时错误视作“底”，甚至显式返回一个“底”，一般用于尚未实现。如声明 f: Bool -\u0026gt; Bool，那么f x = undefined 甚至 f = undefined(“底”也是Bool-\u0026gt;Bool的子类型)\nFunctions that may return bottom are called partial, as opposed to total functions,which return valid results for every possible argument.\n可能会返回“底”的函数是偏函数，与全函数相对，后者对于每个可能参数都会返回一个有效结果。\n由于“底”的存在，因此Haskell类型与函数的范畴称为“Hask”，而不是“Set”。\nWhy Do We Need a Mathematical Model? 程序员一般很熟悉语言的语法。这里关注的是语言的语义，这一点很难描述。\n有些形式化工具可描述语义，但是特别复杂，有一个工具叫做操作语义（operational semantic），它描述的是程序的执行机制。它定义了形式化理想化的解释器。工业级语言的语义，类似 C++，用的是非正式的操作语义，称之为『抽象机器』。使用操作语义存在的问题是要难以证明程序的正确性。要展现一个程序的性质，你只能在一个理想化的解释器中去『运行』它。\n还有一种选择，它叫指称语义（Denotational semantic），是基于数学的。在指称语义中，每个编程结构都会被给出数学解释。使用它，如果你想证明程序的正确性，只需要证明一个数学定理。\nHaskell 是符合指称语义的语言，考虑用 Haskell 定义一个阶乘函数：\n1 fact n = product [1..n] 表达式 [1..n] 是一个从 1 到 n 的整型数列表。product 函数可以将列表中的所有元素相乘。这跟数学课本里的阶乘几乎别无二致。对比C语言的实现\n1 2 3 4 5 6 7 int fact(int n) { int result = 1; for(int i = 2; i \u0026lt;= n; i++) { result *= i; } return result; } 阶乘函数本身就有着明确的数学定义，所以以上代码显得差异巨大。\n但是，如果这么问：从键盘读取字符或者通过网络发送数据包，它们有数学模型么？在非常漫长的时间里，此类问题一直都是很尴尬的问题，答案只能是弯弯绕的那种。似乎指称语义不能极好的应用于一些重要的任务，而编程本来就是围绕这些任务而生。操作语义很容易胜任这些任务。直到范畴论的出现才找到摆脱这种尴尬境地的突破口。Eugenio Moggi 发现可通过单子完成此类任务，这一发现不仅扭转了乾坤，使得指称语义大放异彩，并使得纯函数式程序变得更为有用，也使得传统的编程范式绽放出新的光芒。单子，我们以后在发展更多的范畴论工具时再予以探讨。\nPure and Dirty Functions 在C++或者其他命令式编程语言中的函数，和数学上的函数，不是同一个东西。数学上的韩式只是值到值的映射。\n可以用编程语言实现一个数学函数。很容易就能实现一个平方函数，接受某个数，返回其平方，别的什么也没变。但是，若这个函数还有其它副作用，那这个平方函数跟数学上的平方函数就不是一回事了。\n编程语言中，某个函数对于同一个输入，总是得到同一个输出，且没有其他副作用，那么就是纯函数。在纯函数式编程语言Haskell中，所有函数都是纯的。在其他编程语言中，我们可以要求自己的代码尽可能纯，用以获取更大的益处。\nExamples of Types 基于类型是集合，可以考虑更生僻的类型。例如，空集这种类型是什么？在Haskell中，空集是Void，这跟C++中的void完全不是一回事。Haskell中的Void是一个完全不存储任何值的类型。我们可以定义一个接受Void的函数，但是永远无法调用它，由于调用这个函数需要提供一个Void类型的值，但这种类型的值不存在；至于返回值，可以是任何类型，反正这函数永远不会运行。Haskell中其命名为absurd:: Void -\u0026gt; a，这是个多态返回类型的函数。这种类型与函数，在逻辑学上有更深入的解释，它们被称为 Curry-Howard 自同态。Void 类型表示谎言，absurd 函数的类型相当于『由谎言可以推出任何结论』，也就是逻辑学中所谓的『爆炸原理』。\n还一个实例相当于单例集合，这是只有一个值的类型，它实际上就是C++中的void（有些语言中称unit）。考虑哪些输入是void和返回是void的函数，输入是void的函数总能被调用，若它是纯函数，那么总能返回相同的结果。如int f44() { return 44; }。已知一个函数如果不接受任何值，那么永远不会被调用。从概念上说，这个函数接受了一个空值（很多语言中用()表示）。\n注意，每个 unit 函数都等同于从目标类型中选取一个值的函数。实际上，可以将f44作为数字44的另一种表示方法。这也演示了如何通过用函数调用来代替显式拿出集合中的元素。从unit到A的函数一对一关联了集合A中的元素。\n以void/unit作为返回类型的函数，在命令式编程世界中，这种函数是纯副作用的函数，跟数学函数完全不是一回事。但是在纯函数式世界中，这种函数不做任何事情，只是把接受的参数丢掉。从数学上讲，这种A -\u0026gt; unit的函数是将集合A中的所有元素映射成单例集合中的值。\n下一个例子是二元集合，在C++中，这个集合被称为bool，在Haskell中，被称为Bool，区别是C++中这是内置的，在Haskell中可以自行定义data Bool = True | False，这个定义这样理解：Bool要么是True，要么是False。理论上，C++中也可以用枚举来定义（C++中enum类型本质为整型）。\n输出Bool的函数称为“谓词”（predicate）。例如isDight这种\nChallenges 1 2 3 4 5 6 7 8 9 let memoize f = let mem = new Dictionary\u0026lt;_, _\u0026gt;() fun x -\u0026gt; if mem.ContainsKey(x) then mem.Item(x) else let y = f x mem.Add(x, y) y 1 2 3 4 5 6 7 8 9 10 11 12 not :: Bool -\u0026gt; Bool not True = False not False = True id :: Bool -\u0026gt; Bool id a = a false :: Bool -\u0026gt; Bool false _ = False true :: Bool -\u0026gt; Bool true _ = True 1 2 3 4 5 6 7 8 9 10 11 12 13 absurd :: Void -\u0026gt; () absurd :: Void -\u0026gt; Bool id :: () -\u0026gt; () true :: () -\u0026gt; Bool false :: () -\u0026gt; Bool discard :: Bool -\u0026gt; () id :: Bool -\u0026gt; Bool not :: Bool -\u0026gt; Bool true :: Bool -\u0026gt; Bool false :: Bool -\u0026gt; Bool ","date":"2020-03-11T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/2.types-and-functions/","title":"1.2 Types and Functions - 类型与函数"},{"content":"Category: The Essence of Composition - 范畴：复合的本质 Link to Blog\n\u0026lt;译\u0026gt; 范畴：复合的本质\nA category consists of objects and arrows that go between them.\n“范畴”是个很简单的概念，一个“范畴”由“对象”和对象间的“箭头”组成。范畴的本质是“复合”。“箭头”是可“复合”的。若已有从对象A到B的箭头和从对象B到C的箭头，那么必然存在一个从A到C的箭头，即前两者的“复合”。\n这里的“对象”并非是面向对象编程世界里的“对象”\n“箭头”学名叫做“态射”。\nArrows as Functions - 箭头作为函数 Arrows also called morphisms, as functions.\n可以把函数想象成箭头。假定有一函数f接受一个A类型的值，返回一个B类型的值；此外还有函数g接受B类型的值返回C类型的值。把f的返回值传给g，这样就完成了复合。\n比如Unix系统中的管道ls | grep xxx，就是把前者的值作为输入传到后面。\n数学中，用g ∘ f来表示函数的复合（注意顺序，复合是从右向左发生的，λx.g(f(x))，可以读作“g after f”）。编程语言中有的直接提供了操作符，例如Haskell 中的.（g . f从右向左）；F#中的\u0026gt;\u0026gt; \u0026lt;\u0026lt;（f \u0026gt;\u0026gt; g g \u0026lt;\u0026lt; f前者从左向右复合，后者从右向左）。有些语言没有直接操作符支持，可以动手写出函数体形如g(f(x))，借助x进行函数复合。\n1 2 3 4 f :: A -\u0026gt; B g :: B -\u0026gt; C g . f :: A -\u0026gt; C $g \\circ f = \\lambda x.g \\lparen f \\lparen x \\rparen \\rparen$\nProperties of Composition - 复合的性质 复合是可结合的。（结合律） Composition is associative.\n1 h ∘ g ∘ f = h ∘ (g ∘ f) = (h ∘ g) ∘ f $h \\circ \\lparen g \\circ f \\rparen$ = $\\lparen h \\circ g \\rparen \\circ f$ = $h \\circ g \\circ f$\n1 2 3 4 5 6 f :: A -\u0026gt; B g :: B -\u0026gt; C h :: C -\u0026gt; D -- equality is not defined for functions h . (g . f) == (h . g) . f == h . g . f 任一对象A，都有一个箭头，它是符合的最小单位。这个箭头从对象出发又指向对象自身。（恒等态射） 这称为$id_A$（identity on A）。在数学定义里，现有f接受A返回B，那么$f \\circ id_A = f$，$id_B \\circ f = f$。\n在编程语言中，可以这么实现T id(T x) { return x; }，当然这里的T一般是个泛型参数，也免得为每个对象都实现一个恒等态射了。\nFor every object A there is an arrow which is a unit of composition.\nThis arrow loops from the object to itself. The unit arrow for object A is called $\\bold{id} _{A}$ (identity on $A$).\nIn math notation, if $f$ goes from $A$ to $B$, then $f \\circ \\bold{id} _A = f$ and $\\bold{id} _B \\circ f = f$\n1 2 3 4 5 id :: a -\u0026gt; a id x = x f . id == f id . f == f A category consists of objects and arrows (morphisms). Arrows can be composed, and the composition is associative. Every object has an identity arrow that servers as unit under composition.\nComposition is the Essence of Programming - 复合是编程的本质 Challenges Implement the identity function\n1 2 // there is an `id` in F# let idm x = x Implement the composition function\n1 2 3 4 5 // right to left let compose g f x = g(f(x)) // in F#, there is an `\u0026gt;\u0026gt;` , compose functions left to right let compose f g x = g(f(x)) test composition function\n1 2 3 4 5 let f (i: int): string = string i compose idm f compose f idm ","date":"2020-03-10T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/mathematics/category-theory/ctfp/part-1/1.category-the-essence-of-composition/","title":"1.1 Category: The Essence of Composition - 范畴：复合的本质"},{"content":"准备开发环境 Windows JDK Scala SDK Spark IDEA WSL / Linux VSCode Remote 在Windows系统配置开发及测试环境\nWindows JDK 安装\n需要下载安装两个版本的JDK：\nJDK 1.7：用于适配公司平台打包程序使用，互联网环境做测试可以先不装。现在很难下到了，内网的话，咱们自己有。 JDK 1.8：本地运行Spark Shell使用，Oracle现在贼了不少，需要Oracle账户才能下载，百度上有很多分享，搜一个就行 对于我们目前的开发和测试，请使用JDK1.8，先不要使用JDK11(虽然它是LTS)。（测试过JDK11+Scala，有的行为不一致，可能是Scala对高版本JDK的优化？？？）\n配置JAVA_HOME环境变量\n将JDK 1.8配置到JAVA_HOME作为全局使用\n注意：Spark运行的时候是查找JAVA_HOME环境变量然后运行，不是从PATH中的java.exe启动\n注意：如果将JDK安装在默认位置，如C:\\Program Files\\Java\\jdk1.8.0_241\\，由于路径中的空格问题，将会导致Windows上的Spark Shell启动失败，将JAVA_HOME环境变量修改成：C:\\PROGRA~1\\Java\\jdk1.8.0_241\\这种形式\n配置PATH环境变量\n这个步骤不是必须，但是建议做，环境变量配置完毕后，可以用java -version命令来验证安装\n1 2 3 4 5 PS C:\\Users\\mxtao\u0026gt; java -version java version \u0026#34;1.8.0_241\u0026#34; Java(TM) SE Runtime Environment (build 1.8.0_241-b07) Java HotSpot(TM) 64-Bit Server VM (build 25.241-b07, mixed mode) PS C:\\Users\\mxtao\u0026gt; Scala SDK 需要下载安装两个版本的Scala SDK：\nScala 2.10.7: 用于适配公司平台打包程序使用 Scala 2.11.12：面向测试环境开发程序使用 将两个Scala SDK的zip下载并解压到某处即可，如C:\\Program Files\\Scala\\，不需要配置环境变量。安装完毕之后，可以进入安装目录尝试启动一下Scala REPL，可以在这个环境尝试一下Scala语言。\n1 2 3 4 5 6 PS C:\\Program Files\\Scala\\scala-2.11.12\\bin\u0026gt; .\\scala Welcome to Scala 2.11.12 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_241). Type in expressions for evaluation. Or try :help. scala\u0026gt; :quit PS C:\\Program Files\\Scala\\scala-2.11.12\\bin\u0026gt; 如果是互联网环境开发，Scala SDK的手动安装可以跳过，使用IDEA来下载Scala SDK，如下图所示。\nSpark 下载安装Spark\n建议从Downloads | Apache Spark找到最新稳定版进行下载，建议选择“Pre-build for Apache Hadoop”版本进行下载，这样避免配置Hadoop环境的麻烦。此处附上国内清华镜像的Spark 2.4.5 \u0026amp; Hadoop 2.7。将这个压缩包解压到某处即可，如C:\\Software\\Spark\\\nSpark目录下的examples是一些样例代码，包含Spark Core、Spark SQL及其它组件的使用样例，可以直接参考使用\n配置WinUtils\n若要使Hadoop在Windows上正常运行，需要做些别的事情。从此处cdarlint/winutils找到对应或者更高版本的winutils下载下来。此处附上该项目的完整ZIP。\n我们前面下载的Spark附带的是Hadoop 2.7，因此我们选择了适配2.7.7版本的winutils，将所有文件放在C:\\Software\\Hadoop\\bin中，此时winutils.exe完整路径为C:\\Software\\Hadoop\\bin\\winutils.exe\n现在开始配置环境变量，新增名为HADOOP_HOME的环境变量，其值为C:\\Software\\Hadoop，然后在PATH环境变量中添加%HADOOP_HOME%\\bin\\\n必须配置HADOOP_HOME环境变量\n启动Spark Shell\n进入上文Spark的安装目录，尝试启动Spark Shell检查安装情况。按Ctrl+D可退出Spark Shell\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PS C:\\Software\\Spark\\spark-2.4.5-bin-hadoop2.7\\bin\u0026gt; ./spark-shell Using Spark\u0026#39;s default log4j profile: org/apache/spark/log4j-defaults.properties Setting default log level to \u0026#34;WARN\u0026#34;. To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel). Spark context Web UI available at http://DESKTOP-9LN67TS.mshome.net:4040 Spark context available as \u0026#39;sc\u0026#39; (master = local[*], app id = local-1581394859054). Spark session available as \u0026#39;spark\u0026#39;. Welcome to ____ __ / __/__ ___ _____/ /__ _\\ \\/ _ \\/ _ `/ __/ \u0026#39;_/ /___/ .__/\\_,_/_/ /_/\\_\\ version 2.4.5 /_/ Using Scala version 2.11.12 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_241) Type in expressions to have them evaluated. Type :help for more information. scala\u0026gt; 观察输出的内容，Spark Shell的Application UI开放在本地4040端口，可以访问看看，如下图所示。由于尚未运行任务，所以暂时没太多实质内容\nIDEA 安装IDEA，安装Scala插件。这里主要解释如何创建一个方便本地测试的项目。\n创建一个Scala项目，JDK版本选择1.8，Scala SDK版本选择2.11.12。创建完毕后，将Spark安装目录下的jars(即C:\\Software\\Spark\\spark-2.4.5-bin-hadoop2.7\\jars)设为项目依赖包。到目前为止，项目配置已完毕，可以直接在本地环境调试Spark项目。下面附上一段代码作为本地项目起点\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import org.apache.spark.sql.SparkSession object Application { def main(args: Array[String]): Unit = { val spark = SparkSession.builder().appName(\u0026#34;LocalSparkApp\u0026#34;).master(\u0026#34;local[*]\u0026#34;).getOrCreate() val sc = spark.sparkContext import spark.implicits._ // val schema = \u0026#34;a INT, b STRING, c DOUBLE\u0026#34; // val df = spark.read.schema(schema).option(\u0026#34;sep\u0026#34;, \u0026#34;\\t\u0026#34;).csv(\u0026#34;/local-path-to-file/\u0026#34;) // df.createOrReplaceTempView(\u0026#34;t\u0026#34;) // spark.sql(\u0026#34;select * from t where a \u0026gt; 10\u0026#34;).show(20) // write your code here ... spark.close() } } 对于我们面向生产的项目，JDK版本1.7，Scala版本2.10.7，依赖jar用生产环境的，然后编译出jar即可。\nWSL / Linux 在纯Windows环境中，无法启动Spark Standalone模式集群，我们尝试从另外的角度解决这一问题。此处假定大家使用Windows10系统。如果是低版本Windows，那也许只能安装虚拟机Linux或者阿里云之类的云服务Linux来启动了。\n启用WSL\n在“控制面板”-“程序和功能”-“启用或关闭Windows功能”找到“适用于Windows的Linux子系统”并点击启用，如下图所示\n或者在管理员权限启动的PowerShell中执行如下命令\n1 Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux 然后重启计算机（界面勾选或者执行命令都需要重启）\n安装一个Linux\n打开Windows10应用商店(无需微软账户也可使用)，在搜索栏输入Ubuntu，建议选择Ubuntu 18.04 LTS安装。安装完成之后，在Windows开始菜单即可找到这一应用，点击打开。首次启动如下图所示，将会要求创建Unix用户名和密码，创建完毕即可正常使用(有时会卡在take a few minutes...一段时间，可以按一下回车。任何时候出现了问题都可以将这个Ubuntu应用卸载然后重新安装)\n配置WSL\n在我们安装的这个Ubuntu 18.04 LTS中，依次键入如下命令进行环境配置，不要粘贴一大段执行（尤其前几个sudo执行的命令），因为要输入刚刚设定的Unix密码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 sudo apt update sudo apt upgrade -y # 更新本地现已安装的软件包 sudo apt install openjdk-8-jdk -y # 安装JDK8 cd ~ # 本地盘会自动挂载到Linux子系统中，例如C盘挂载于 /mnt/c/，找到Windows系统的Spark，放到Linux中用 # 也可以将原来的tgz包重新解压到Linux系统某处，作为Linux自己的安装，如/opt/spark/ echo \u0026#34;export SPARK_HOME=/mnt/c/Software/Spark/spark-2.4.5-bin-hadoop2.7/\u0026#34; \u0026gt;\u0026gt; .profile echo \u0026#39;export PATH=$PATH:$SPARK_HOME/bin\u0026#39; \u0026gt;\u0026gt; .profile source .profile ln -s /mnt/c/Software/Spark/spark-2.4.5-bin-hadoop2.7/sbin spark-sbin cd spark-sbin ./start-master.sh # 启动Spark Master，此时可以查看localhost:8080查看Spark Master UI，从此处得到master url ./start-slave.sh spark://DESKTOP-9LN67TS.localdomain:7077 # 启动Spark Worker，刷新MasterUI，看到有了个Worker cd ~ spark-shell --master spark://DESKTOP-9LN67TS.localdomain:7077 # 启动集群模式的Spark Shell，刷新MasterUI，看到有Application 出现 cd spark-sbin ./stop-slave.sh # 停止Spark Master ./stop-master.sh # 停止Spark Worker # 不要用start-all.sh/stop-all.sh/start-slaves.sh/stop-slaves.sh，在WSL上，没有开启22端口，执行这几个命令会出错 WSL依然存在问题，比如最显著的IO性能问题、某些Linux应用尚不兼容问题(WSL2将着手解决)，但对我们而言，只是构造出一个开发测试环境，我认为这些问题可以不用在意，避免了安装虚拟机的各种麻烦，这点程度的损失是可以接受的。若是无法安装/配置WSL，而是有一个Linux机器，环境配置流程是一样的，按照 安装JDK -\u0026gt; 安装Spark -\u0026gt; 启动Spark 的流程进行即可，几个主要Web UI如下所示：\nSpark Master Web UI Spark Worker Web UI Spark Shell Web UI 至此，我们的已经配置启动了Standalone模式的Spark集群，之后可以向这个集群提交任务并运行。一般的提交命令如下所示：\n1 spark-submit --master spark://hostname:port --deploy-mode cluster --class xxx.spark.Application SparkApp.jar 任务提交运行完毕后，Spark Master UI如下所示，可以通过界面上相应App的链接去查看之前任务运行的日志。\nVSCode Remote 需要互联网连接\n由于Windows上开发环境配置还是有些麻烦，测试环境/仅尝试下Spark的话，可以考虑使用Visual Studio Code进行远程开发(参考VS Code Remote Development)\nWindows上安装好了VSCode后，在WSL/Linux上安装JDK + Spark + Maven即可开始。用到的VSCode插件有Remote WSL/Remote SSH和Scala(Metals)，这种方式的话，对于Windows环境将不再有任何要求\n","date":"2020-03-09T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/platform/spark/spark-dev-tutorial/1-environment-prepare/","title":"1. 准备开发环境"},{"content":"Scala Intro Scala是一门功能特别丰富的编程语言，我们涉及到的只是其一个较小的子集，此处列出相关文档，可以自行学习\nScala官方文档也提供了中文版，但有的翻译感觉还是有点生硬。此处将仅给出中文版的URL，如果要访问英文版，将URL中zh-cn去掉即可。\n可以先看一下A Scala Tutorial for Java Programmers，这篇文档没有简体中文，有繁体中文版，但是很多概念跟我们说的名字不一样，要不直接看英文版得了。\n官方提供了Tour of Scala系列文档来介绍语言的核心功能，这里列出这系列文档的子集，因为我们用到的也仅是个子集\n基础: 最基本的语言基础，告诉我们表达式怎么写、函数怎么定义、类如何定义等 统一类型: 介绍了Scala类型层次结构 类: 定义一个类及其成员 元组: 需要了解是什么，怎么用 高阶函数: 好像有点厉害，只要能理解了函数可以作为参数和返回值即可 单例对象: 这个概念最好搞明白 泛型类: 泛型使用 类型推断: 由此，我们很多地方不需要写类型 包和导入: 跟java差不多 以上是我们用到的，搞懂以上概念，目前我们写的大部分代码看懂应该没什么问题，如果有时间还是建议把全系列翻一遍\nScala语法速查\n","date":"2020-03-09T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/platform/spark/spark-dev-tutorial/2-scala-intro/","title":"2. Scala Intro"},{"content":"使用 Spark 进行数据分析 Spark简介 Spark处理数据 Spark Core Spark SQL 开发一个Spark程序 开发常见问题 Spark简介 Apache Spark是个分布式计算框架，其提供了一大批高级API基于批处理或流式处理对大规模数据做ETL、机器学习和图处理等。可以用来开发Spark程序的编程语言有Scala、Python、Java、R和SQL。也可以将其理解成一个具备批处理和流处理能力的分布式数据处理引擎，支持SQL查询、图处理和机器学习等。Spark平台的组件间关系如下图所示：\nSpark Core是整个Spark平台的核心部分，实现了重要的基础功能，例如输入输出、任务分发和调度、错误恢复等。Spark Core定义了一个特殊的数据结构RDD，并提供了一系列操作RDD的编程API。\nSpark SQL是基于Spark API开发的能进行关系型数据处理的组件。该组件赋予Spark开发者对关系型数据处理的能力，也能让SQL用户能进行基于Spark的复杂分析。与Spark Core相比，Spark SQL是个从关系型视角对(半)结构化数据进行处理的框架，我们可以用SQL及SQL风格API来描述业务逻辑。\nSpark Architecture Spark程序可以看作是一组互相独立地跑在集群上的进程，这组进程接受driver进程(main所处的进程)中的SparkContext对象协调的。其简单结构如下图所示：\nCluster Manger负责为应用分配集群的资源，SparkContext适配了多种类型的集群（如Spark Standalone、Apache Mesos、Hadoop YARN、Kubernetes，我们实际情况是YARN）。Spark程序运行时，SparkContext首先去找Cluster Manager请求资源(executor，可以执行计算及存储应用数据的进程)，然后SparkContext将程序代码分发到各个executor，最后SparkContext把task发送到各个executor去执行。\n需要指出以下事项：\n每个Spark程序获得的是属于自己的executors，这些executors在整个程序运行过程中保持运行，以多线程方式处理任务。这个特点使多个Spark程序在调度侧（每个driver只调度自己的task）和执行侧（不同程序的task跑在不同的JVM里）都是互相隔离的。当然了，如果不把数据持久化到外部，那么不同的Spark程序也就无法共享数据。 Spark对于集群管理器并不关心，只关心自己要来的executor。 程序整个生命周期内，driver必须一直监听着executor发起的连接，这意味这driver和executors是必须网络通畅的。 RDD RDD(即弹性分布式数据集)是Spark Core中的核心概念之一，是Spark最重要的数据抽象，所有数据处理都是基于对RDD的处理来实现。。从名字上体现出了其一些特点\n弹性：这里的意思侧重“可恢复”，而不是“可伸缩”。RDD能从各种意外(比如节点挂掉、executor被误杀等)导致的数据丢失或损坏中进行重新计算，是支持错误恢复的 分布式：数据分布在集群的各个节点中 数据集：数据的集合，数据可以是简单的数值，也可以是更复杂的类型，例如Tuple、List、Map等数据结构，甚至可以是自行定义的类对象 除此之外，RDD有一些其他特点，这里列出几个\n内存优先：Spark程序运行时，RDD中的数据会尽可能多且尽可能久地保存在内存中 不可变/只读：RDD一旦创建就不再可变，只能通过变换生成一个新的RDD 延迟计算：在一个action触发了任务实际运行之前，RDD中的数据尚不可及 可缓存：可以将RDD中全部数据持久化到某个存储，例如内存(默认及绝大多数情况)或者硬盘(由于硬盘存取速度问题，一般很少这么做) 并行化：对RDD数据的处理是并行的 类型化：RDD中的数据是有类型的，例如Long类型数据放在RDD[Long]中，Student对象放在RDD[Student]中 RDD支持两种类型的操作：transformation（转换）和action（动作）。从现有RDD生成新RDD的操作称为transformation，从RDD上运行执行计算然后求得一个值的操作称为action。transformation是延迟计算的。当对一个RDD执行了transformation之后，便构造出了一个“RDD血缘图”，记录了对RDD的变换操作和依赖信息。我们需要对依赖情况做到心里有数，宽窄依赖如下所示：\nResilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing\nDataset[T]\u0026amp;DataFrame 对于Spark SQL而言，Dataset[T]是其核心类之一，所有的操作都是对Dataset的处理(提交的SQL也会首先被解析成操作Dataset)。在内部，所有对Dataset的操作最终转换成RDD的操作。\nDataset是个分布式的数据集合。它是Spark1.6新增的接口，综合了RDD的优势(强类型，强大的λ函数能力)和Spark SQL的优化执行引擎优势。DataFrame是个数据已进行字段命名的Dataset。在Spark SQL中DataFrame=Dataset[Row]\n对Dataset的操作也可以分transformation和action两类。其中变换操作(transformation)，分为“typed”和“untyped”两类。所谓“typed”，指的是编译时已知要处理的数据的类型(例如操作Dataset[Student])；相对应的，“untyped”指的是编译时不知道要处理的数据的类型（操作DataFrame）。\n用Spark处理数据 对于有一些经验的同事，可以考虑深入研究Spark Core部分的算子，对比其使用场景；对于入门且马上要进行数据分析的同事，可以考虑直接使用Spark SQL，只需要一小部分的编程即可，数据分析可以直接以SQL实现。\n另一方面，由于Spark Core与Spark SQL可以组合使用(基于Dataset/DataFrame/RDD的互相转换)，有经验的同事可以尝试各种操作，入门的话还是建议一步步来。\n获取SparkContext\u0026amp;SparkSession对象 SparkContext是Spark服务的入口点，堪比Spark程序的心脏；SparkSession是Spark SQL的入口点。\n构造/获取SparkContext对象的方式有很多，例如下面通过SparkConf来构造，但这种方式我用得不多了\n1 2 3 import org.apache.spark.{SparkConf, SparkContext} val conf = new SparkConf().setMaster(\u0026#34;local[*]\u0026#34;).setAppName(\u0026#34;LocalSparkApp\u0026#34;) val sc = SparkContext.getOrCreate(conf) 开发应用程序时，一般以如下方式获取两者的对象。\n1 2 3 4 5 6 7 import org.apache.spark.sql.SparkSession // local spark application val spark = SparkSession.builder().appName(\u0026#34;LocalSparkApp\u0026#34;).master(\u0026#34;local[*]\u0026#34;).getOrCreate() // spark application val spark = SparkSession.builder().appName(\u0026#34;SparkApp\u0026#34;).master(\u0026#34;yarn\u0026#34;).getOrCreate() val sc = spark.sparkContext 建议：将SparkSession对象命名为spark、将SparkContext对象命名为sc，使之与Spark-Shell环境保持一致。这样核心逻辑代码可以直接拿到Spark-Shell中使用。\nSpark-Shell会直接初始化这两者给我们使用(控制台输出的6-7行)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PS C:\\Software\\Spark\\spark-2.4.5-bin-hadoop2.7\\bin\u0026gt; ./spark-shell Using Spark\u0026#39;s default log4j profile: org/apache/spark/log4j-defaults.properties Setting default log level to \u0026#34;WARN\u0026#34;. To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel). Spark context Web UI available at http://DESKTOP-9LN67TS.mshome.net:4040 Spark context available as \u0026#39;sc\u0026#39; (master = local[*], app id = local-1581394859054). Spark session available as \u0026#39;spark\u0026#39;. Welcome to ____ __ / __/__ ___ _____/ /__ _\\ \\/ _ \\/ _ `/ __/ \u0026#39;_/ /___/ .__/\\_,_/_/ /_/\\_\\ version 2.4.5 /_/ Using Scala version 2.11.12 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_241) Type in expressions to have them evaluated. Type :help for more information. scala\u0026gt; Spark Core Spark Core程序始于SparkContext对象sc。RDD是Spark Core的核心类之一，所有数据处理都是基于对RDD的处理来实现。对其操作分为两类：transformation（转换）和action（动作）。transformation是生成新RDD的操作，例如map filter reduceByKey等，action是得出一个非RDD值的操作，比如count collect take saveAsTextFile等。\n准备数据(构造RDD) parallelize方法是从序列创建一个RDD，生产中一般不用，仅作学习和测试使用。\n1 2 val rawStr = \u0026#34;Hello World!\\naaa bbb ccc ddd\\naaa aaa\\nbbb\\nccc\\n\u0026#34; val lineRdd = sc.parallelize(rawStr.lines.toSeq) textFile方法是读取文本文件创建RDD，RDD中每个元素都是文件中的一行\n1 val fileRdd = sc.textFile(\u0026#34;/path-to-file/\u0026#34;) 处理数据(操作RDD) 此处简单介绍下常见的算子，我们目前工作中常用的算子有map filter distinct reduceByKey join\nmap算子：将RDD中的每个元素转换成另一种状态，目标状态不设限\n1 2 3 4 5 6 7 /* * lineRdd -\u0026gt; [\u0026#34;Hello World!\u0026#34;, \u0026#34;aaa bbb ccc ddd\u0026#34;, \u0026#34;aaa aaa\u0026#34;, ...] * * lowerRdd -\u0026gt; [\u0026#34;hello world!\u0026#34;, \u0026#34;aaa bbb ccc ddd\u0026#34;, \u0026#34;aaa aaa\u0026#34;, ...] */ // val lowerRdd = lineRdd.map(line =\u0026gt; line.toLowerCase()) val lowerRdd = lineRdd.map(_.toLowerCase) flatMap算子：将RDD中的每个元素转换成一个可枚举的序列，然后将这些序列“压平”(也可以理解成连接起来)\n1 2 3 4 5 6 /* * lineRdd -\u0026gt; [\u0026#34;Hello World!\u0026#34;, \u0026#34;aaa bbb ccc ddd\u0026#34;, \u0026#34;aaa aaa\u0026#34;, ...] * * wordRdd -\u0026gt; [\u0026#34;Hello\u0026#34;, \u0026#34;World!\u0026#34;, \u0026#34;aaa\u0026#34;, \u0026#34;bbb\u0026#34;, ...] */ val wordRdd = lineRdd.flatMap(_.split(\u0026#34; \u0026#34;)) filter算子：检查每个元素，过滤掉不符合条件的元素\n1 2 3 4 5 6 /* * wordRdd -\u0026gt; [\u0026#34;Hello\u0026#34;, \u0026#34;World!\u0026#34;, \u0026#34;aaa\u0026#34;, \u0026#34;bbb\u0026#34;, ...] * * pureWordRdd -\u0026gt; [\u0026#34;aaa\u0026#34;, \u0026#34;bbb\u0026#34;, \u0026#34;ccc\u0026#34;, \u0026#34;ddd\u0026#34;, \u0026#34;aaa\u0026#34;, ...] */ val pureWordRdd = wordRdd.filter(_.toCharArray.toSet.size == 1) distinct算子：把数据集中的数据去重\n1 2 3 4 5 6 /* * pureWordRdd -\u0026gt; [\u0026#34;aaa\u0026#34;, \u0026#34;bbb\u0026#34;, \u0026#34;ccc\u0026#34;, \u0026#34;ddd\u0026#34;, \u0026#34;aaa\u0026#34;, ...] * * distinctRdd -\u0026gt; [\u0026#34;aaa\u0026#34;, \u0026#34;bbb\u0026#34;, \u0026#34;ccc\u0026#34;, \u0026#34;ddd\u0026#34;] */ val distinctRdd = pureWordRdd.distinct() count算子：统计RDD中数据的条数\n1 distinctRdd.count() take算子：从RDD中取指定数量的数据放到数组中\n1 distinctRdd.take(5).foreach(println) collect算子：将RDD中的所有数据收集到数组中（实际不太常用，除非能保证RDD中的数组足够少，能放到一个数组中去）\n1 distinctRdd.collect.foreach(println) groupBy算子：将RDD中的数据分组\n1 2 3 4 5 6 /* * pureWordRdd -\u0026gt; [\u0026#34;aaa\u0026#34;, \u0026#34;bbb\u0026#34;, \u0026#34;ccc\u0026#34;, \u0026#34;ddd\u0026#34;, \u0026#34;aaa\u0026#34;, ...] * * groupedRdd -\u0026gt; [\u0026#39;a\u0026#39;-\u0026gt;[\u0026#34;aaa\u0026#34;, \u0026#34;aaa\u0026#34;,...], \u0026#39;b\u0026#39;-\u0026gt;[\u0026#34;bbb\u0026#34;,...], \u0026#39;c\u0026#39;-\u0026gt;[\u0026#34;ccc\u0026#34;], ...] */ val groupedRdd = pureWordRdd.groupBy(_.charAt(0)) 归纳类算子：reduce fold aggregate。这组算子的目标是将RDD归纳成一个值。下面演示其各自用法(功能目标一致，求RDD中字符串长度之和)\n1 2 3 4 5 6 // def reduce(f: (T, T) =\u0026gt; T): T pureWordRdd.map(_.length).reduce(_ + _) // def fold(zeroValue: T)(op: (T, T) =\u0026gt; T): T pureWordRdd.map(_.length).fold(0)(_ + _) // def aggregate[U](zeroValue: U)(seqOp: (U, T) =\u0026gt; U, combOp: (U, U) =\u0026gt; U): U pureWordRdd.aggregate(0)((i, s) =\u0026gt; i + s.length, _ + _) 交并差算子：intersection union subtract。同数学上交集、并集、差集的概念\n1 2 3 4 5 6 val rdd1 = sc.parallelize(Seq(\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;)) // [A, B, C] val rdd2 = sc.parallelize(Seq(\u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;, \u0026#34;D\u0026#34;)) // [B, C, D] rdd1.intersection(rdd2) // rdd1 ∩ rdd2 = [B, C] rdd1 ++ rdd2 // rdd1 ∪ rdd2 = [A, B, C, B, C, D] rdd1.union(rdd2) // rdd1 ∪ rdd2 = [A, B, C, B, C, D] rdd1.subtract(rdd2) // rdd1 - rdd2 = [A] 以上算子是操作RDD[T]时常用的，当T是个二元组时，此时RDD[(K, V)]将会被隐式转换成PairRDDFunctions[K, V]对象，此时可以使用一些特殊算子，例如以下介绍的join系算子和xxxByKey系算子\n归纳类算子：reduceByKey foldByKey aggregateByKey。下面演示其用法(统计各个Key的个数)\n1 2 3 4 5 6 7 8 val rdd = sc.parallelize(Seq((\u0026#34;A\u0026#34;, \u0026#34;AAA\u0026#34;), (\u0026#34;A\u0026#34;, \u0026#34;aaa\u0026#34;), (\u0026#34;B\u0026#34;, \u0026#34;bbb\u0026#34;))) // def reduceByKey(func: (V, V) =\u0026gt; V): RDD[(K, V)] rdd.map(t =\u0026gt; (t._1, 1)).reduceByKey(_ + _) // def foldByKey(zeroValue: V)(func: (V, V) =\u0026gt; V): RDD[(K, V)] rdd.map(t =\u0026gt; (t._1, 1)).foldByKey(0)(_ + _) // def aggregateByKey[U](zeroValue: U)(seqOp: (U, V) =\u0026gt; U, combOp: (U, U) =\u0026gt; U): RDD[(K, U)] rdd.aggregateByKey(0)((i, _) =\u0026gt; i + 1, _ + _) groupByKey算子：该算子不再需要参数，行为与上述groupBy算子行为一致\n关联类算子：join leftOuterJoin rightOuterJoin fullOuterJoin，主要区别在于对于关联失败数据的处理方式\n1 2 3 4 5 6 7 val rdd1 = sc.parallelize(Seq((\u0026#34;A\u0026#34;, \u0026#34;aaa\u0026#34;), (\u0026#34;B\u0026#34;, \u0026#34;bbb\u0026#34;), (\u0026#34;C\u0026#34;, \u0026#34;ccc\u0026#34;))) val rdd2 = sc.parallelize(Seq((\u0026#34;B\u0026#34;, \u0026#34;BBB\u0026#34;), (\u0026#34;C\u0026#34;, \u0026#34;CCC\u0026#34;), (\u0026#34;D\u0026#34;, \u0026#34;DDD\u0026#34;))) rdd1.join(rdd2) // [(\u0026#34;B\u0026#34;, (\u0026#34;bbb\u0026#34;, \u0026#34;BBB\u0026#34;)), (\u0026#34;C\u0026#34;, (\u0026#34;ccc\u0026#34;, \u0026#34;CCC\u0026#34;))] rdd1.leftOuterJoin(rdd2) // [(\u0026#34;A\u0026#34;, (\u0026#34;aaa\u0026#34;, None)), (\u0026#34;B\u0026#34;, (\u0026#34;bbb\u0026#34; ,Some(\u0026#34;BBB\u0026#34;))), (\u0026#34;C\u0026#34;, (\u0026#34;ccc\u0026#34;,Some(\u0026#34;CCC\u0026#34;)))] rdd1.rightOuterJoin(rdd2) // [(\u0026#34;B\u0026#34;, (Some(\u0026#34;bbb\u0026#34;), \u0026#34;BBB\u0026#34;)), (\u0026#34;C\u0026#34;, (Some(\u0026#34;ccc\u0026#34;), \u0026#34;CCC\u0026#34;)), (\u0026#34;D\u0026#34;, (None, \u0026#34;DDD\u0026#34;))] rdd1.fullOuterJoin(rdd2) // [(\u0026#34;A\u0026#34;, (Some(\u0026#34;aaa\u0026#34;), None)), (\u0026#34;B\u0026#34;, (Some(\u0026#34;bbb\u0026#34;), Some(\u0026#34;BBB\u0026#34;))), (\u0026#34;C\u0026#34;, (Some(ccc), Some(\u0026#34;CCC\u0026#34;))), (\u0026#34;D\u0026#34;, (None, Some(\u0026#34;DDD\u0026#34;)))] 以上算子便是离线数据分析常用的算子，对于数据进行的分析都是基于这些算子的组合使用，常用才能熟悉。关于以上列出算子的官方文档，请参考ScalaDoc：RDD，PairRDDFunctions\n保存数据(保存RDD) 对于结果数据保存，我们常用的是saveAsTextFile方法，在调用此方法之前，需要保证数据已经是\\t分隔字段的单行字符串形式，一般需要用map算子转换一下数据的形式，如下所示：\n1 rdd.map(_.mkString(\u0026#34;\\t\u0026#34;)).saveAsTextFile(\u0026#34;/path-to-result/\u0026#34;) Spark SQL 此处介绍Spark SQL主要为了降低数据处理的门槛，基于我们已经很熟悉的SQL语言，马上就能进行基本的数据处理工作。因此侧重以SQL方式进行数据处理，对DatasetAPI细节介绍较少，若要以编程方式使用，可参考其文档ScalaDoc - Dataset\n同样的逻辑，使用DatasetAPI和SQL表达在性能上没有任何差别。\n首先给出一段简单代码，用以揭示使用Spark SQL进行数据分析的一般流程\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // --------------- preparing --------------- // construct schema val schema = new StructType() .add(\u0026#34;id\u0026#34;, DataTypes.LongType, nullable = true) .add(\u0026#34;name\u0026#34;, DataTypes.StringType, nullable = true) .add(\u0026#34;age\u0026#34;, DataTypes.IntegerType, nullable = true) .add(\u0026#34;score\u0026#34;, DataTypes.DoubleType, nullable = true) // load file, create dataframe val df = spark.read .schema(schema) .option(\u0026#34;header\u0026#34;, false) // default is false .option(\u0026#34;inferSchema\u0026#34;, false) // default is false .option(\u0026#34;sep\u0026#34;, \u0026#34;\\t\u0026#34;) .csv(\u0026#34;/path-to-bcp-file/\u0026#34;) // register as a temp view/table df.createOrReplaceTempView(\u0026#34;tableName\u0026#34;) // --------------- processing --------------- // execute sql statement val result = spark.sql(\u0026#34;select * from tableName where name is not null and age \u0026gt; 18 and score \u0026gt; 60\u0026#34;) // --------------- saving --------------- result.write.option(\u0026#34;sep\u0026#34;, \u0026#34;\\t\u0026#34;).csv(\u0026#34;/path-to-result-dir/\u0026#34;) // --------------- exit --------------- spark.close() 我们主要对数据准备、处理、保存阶段进行详细的讨论。\nPreparing 数据准备阶段的工作是告知Spark SQL：我们的数据是什么结构，怎么样能拿到。解答这两个问题的办法有很多，我们视情况选择自己用得顺手的即可。此处给出两类办法以供参考。\n定义业务数据类型 1 2 3 4 5 6 7 8 9 10 11 12 // define class case class Student(id: String, name: String, age: Int, score: Double) // read file val ds = sc.textFile(\u0026#34;/path-to-file/\u0026#34;) // type: RDD[String] sample: \u0026#34;001,tom,18,60.0\u0026#34; .map(_.split(\u0026#34;,\u0026#34;)) // type: RDD[Array] sample: [\u0026#34;001\u0026#34;,\u0026#34;tom\u0026#34;,\u0026#34;18\u0026#34;,\u0026#34;60.0\u0026#34;] .map(arr =\u0026gt; (arr(0), arr(1), arr(2).toInt, arr(3).toDouble)) // type: RDD[Tuple] sample: (\u0026#34;001\u0026#34;,\u0026#34;tom\u0026#34;,18,60.0) .map(t =\u0026gt; Student(t._1, t._2, t._3, t._4)) // type: RDD[Student] sample: Student(\u0026#34;001\u0026#34;,\u0026#34;tom\u0026#34;,18,60.0) .toDS() // type: Dataset[Student] // register as a temp view/table ds.createOrReplaceTempView(\u0026#34;tableName\u0026#34;) 分析以上代码，首先定义了一个Student类型，我们读取进来的数据要转换成此类型的一组实例。然后我们用Spark Core的map算子进行数据格式的转换，转换成了一批Student对象放在了RDD中，最后转换成Dataset[Student]类型。此时，Spark SQL的两个问题回答完毕：要操作的数据Student(字段名称和字段类型已知)，要操作的数据从上游RDD直接可取。我们将这个Dataset[Student]类型的对象注册到Spark SQL系统供下面的代码使用\n注1：Student类的定义不能放到方法体里\n注2：要调用toDS方法，需要在调用前(一般在SparkSession的对象spark构造完毕后)导入一些隐式成员，即加上以下语句import spark.implicits._\n构造Schema Spark SQL使用Schema对数据的结构进行描述，Schema包含列名、数据类型、是否可空。我们可以直接构造出一个Schema告知Spark SQL我们的数据是什么结构。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // construct schema val schema = new StructType() .add(\u0026#34;id\u0026#34;, DataTypes.LongType) .add(\u0026#34;name\u0026#34;, DataTypes.StringType) .add(\u0026#34;age\u0026#34;, DataTypes.IntegerType) .add(\u0026#34;score\u0026#34;, DataTypes.DoubleType) // read file val rdd = sc.textFile(\u0026#34;/path-to-file/\u0026#34;) // type: RDD[String] sample: \u0026#34;001,tom,18,60.0\u0026#34; .map(_.split(\u0026#34;,\u0026#34;)) // type: RDD[Array] sample: [\u0026#34;001\u0026#34;,\u0026#34;tom\u0026#34;,\u0026#34;18\u0026#34;,\u0026#34;60.0\u0026#34;] .map(arr =\u0026gt; (arr(0), arr(1), arr(2).toInt, arr(3).toDouble)) // type: RDD[Tuple] sample: (\u0026#34;001\u0026#34;,\u0026#34;tom\u0026#34;,18,60.0) .map(Row.fromSeq(_)) // type: RDD[Row] val df = spark.createDataFrame(rdd, schema) // register as a temp view/table df.createOrReplaceTempView(\u0026#34;tableName\u0026#34;) 分析以上代码，我们首先构造了一个StuctType对象来描述数据结构，然后读取文件，与上面不同的是，最终转换成RDD[Row]类型，然后调用createDataFrame方法。createDataFrame方法的两个参数一个回答了数据来源的问题，另一个回答了数据结构的问题。\n注1：文件加载部分可以进行简化\n1 2 val df = spark.read.schema(schema).option(\u0026#34;sep\u0026#34;, \u0026#34;\\t\u0026#34;).csv(\u0026#34;/path-to-bcp-file/\u0026#34;) df.createOrReplaceTempView(\u0026#34;tableName\u0026#34;) 使用了Spark SQL内置的一些工具进行简化。schema方法指定了要加载的数据的结构，csv方法指出了数据文件的格式，我们通过option方法指定一些选项(此处指定了分隔符以适配bcp文件)。加载选项的详细列表请参考ScalaDoc - DataFrameReader\n注2：构造Schema部分可以进行简化\n其实DataFrameReader已有类似API，即spark.schema(\u0026quot;id LONG, name STRING, ...\u0026quot;).option(...)...，其中schema方法接受Schema字符串并自动转换，但这个API是Spark 2.3.0新增，内部平台版本应该是2.1.x，还没有此API\n鉴于此，我们可以实现一个SchemaInterpolator使之更易用，最终效果如下所示：\n1 2 import SchemaInterpolator._ val schema = schema\u0026#34;id long, name string, age int, score double\u0026#34; SchemaInterpolator的简易实现已附在本文档\n注3：自描述数据源\n有些数据源能描述自己的数据结构，例如jdbc json csv parquet等，这种情况我们也可以让Spark SQL自行推断数据结构，以csv文件为例\n1 2 3 4 5 val df = spark.read .option(\u0026#34;header\u0026#34;, true) .option(\u0026#34;inferSchema\u0026#34;, true) .csv(\u0026#34;/path-to-bcp-file/\u0026#34;) df.createOrReplaceTempView(\u0026#34;tableName\u0026#34;) Processing 准备阶段已经将临时视图注册到了Spark SQL系统中，我们可以直接提交要处理的SQL。此处仅给出一些样例，旨在揭示Spark SQL的各种可能，一般我们能想到的操作，都是可以做到的\nspark.sql(sqlText)的执行结果是DataFrame(即Dataset[Row])，可以将结果保存、注册为新视图、替换之前的视图等 1 2 3 4 5 6 7 val df = ... // create view df.createOrReplaceTempView(\u0026#34;t1\u0026#34;) val df1 = spark.sql(\u0026#34;select * from t1 where t1.xxx is not null and ...\u0026#34;) // replace view df1.createOrReplaceTempView(\u0026#34;t1\u0026#34;) 这段代码执行完毕之后，\u0026ldquo;t1\u0026quot;已经变成了筛选之后的数据，直接用就可以，因此，无需总是为给表取名字挠头\n多数据集join是可以的，sql直接能做 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import org.apache.spark.sql._ import org.apache.spark.sql.types._ import spark.implicits._ val schema = new StructType().add(\u0026#34;k\u0026#34;, DataTypes.StringType).add(\u0026#34;v\u0026#34;, DataTypes.StringType) val rdd1 = sc.parallelize(Seq((\u0026#34;A\u0026#34;, \u0026#34;aaa\u0026#34;), (\u0026#34;B\u0026#34;, \u0026#34;bbb\u0026#34;), (\u0026#34;C\u0026#34;, \u0026#34;ccc\u0026#34;))).map(Row.fromTuple(_)) val rdd2 = sc.parallelize(Seq((\u0026#34;B\u0026#34;, \u0026#34;BBB\u0026#34;), (\u0026#34;C\u0026#34;, \u0026#34;CCC\u0026#34;), (\u0026#34;D\u0026#34;, \u0026#34;DDD\u0026#34;))).map(Row.fromTuple(_)) spark.createDataFrame(rdd1, schema).createOrReplaceTempView(\u0026#34;t1\u0026#34;) spark.createDataFrame(rdd2, schema).createOrReplaceTempView(\u0026#34;t2\u0026#34;) val df = spark.sql(\u0026#34;select t1.k as k, t1.v as v1, t2.v as v2 from t1 join t2 on t1.k = t2.k\u0026#34;) 更多JOIN操作请参考：LanguageManual Joins\nSpark SQL内置函数和运算符 Spark SQL提供了一批函数和运算符，我们可以直接在SQL里面使用，下面给出两个例子\n例子1：过滤掉某字段的字符串长度小于10的数据\n1 select * from t1 where length(t1.filed0) \u0026gt;= 10 例子2：根据出生日期，算出年龄\n1 select *, int(date_format(now(), \u0026#39;yyyy\u0026#39;)) - int(year(t.birth)) as age from t Spark SQL的内置函数列表请参考：Spark SQL, Built-in Functions\nUDF(用户自定义函数) 思考前面的例子2，根据出生日期计算年龄，用代码写我们有无数种实现方式，但是只用内置函数，好像很麻烦，所以我们实现个UDF，如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // define an normal function val calcAge: String =\u0026gt; Int = (birth: String) =\u0026gt; { import java.text.SimpleDateFormat import java.util.Date val now = new Date() val fmt = new SimpleDateFormat(\u0026#34;yyyy\u0026#34;) val thisYear = fmt.format(now).toInt val birthYear = birth.substring(0, 4).toInt thisYear - birthYear } // register udf spark.udf.register(\u0026#34;calcAge\u0026#34;, calcAge) // use it! spark.sql(\u0026#34;select *, calcAge(t.birth) as age from t\u0026#34;) 分析以上代码，流程很清晰，写个普通函数 -\u0026gt; 注册到Spark SQL -\u0026gt; 直接在SQL中使用。\n注意：尽量不要用这种方式！UDF对Spark SQL来说就是个黑盒，它无法对此做出优化，复杂UDF很有可能造成性能瓶颈。除非别无选择，否则不要用UDF\n一些有用的文档 Spark SQL与Apache Hive的兼容性：Compatibility with Apache Hive\nHive语言手册(我们主要关注SQL部分)：LanguageManual\nSELECT：LanguageManual Select\nSaving 数据保存的做法依然有很多，考虑到我们的实际需求，此处只推荐一种做法，如下所示\n1 2 val df = spark.sql(\u0026#34;select field1, field2, ... from t\u0026#34;) df.write.option(\u0026#34;sep\u0026#34;, \u0026#34;\\t\u0026#34;).csv(\u0026#34;/path-to-result-dir/\u0026#34;) 实际进行保存之前，建议执行一次筛选操作，用以确认要保存的字段并且保证字段的顺序，然后执行保存。\n更多数据保存的相关选项，请参考ScalaDoc - DataFrameWriter\nSchemaInterpolator 一个基于StringInterpolator的SchemaInterpolator实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 object SchemaInterpolator { import scala.util.{Try, Failure, Success} import org.apache.spark.sql.types._ implicit class SparkSQLSchema(val sc: StringContext) { // InnerStructField -\u0026gt; index option, field private type InnerStructField = (Option[Int], StructField) // val schema = schema\u0026#34;a int, b string false, c double true some-comment\u0026#34; // val schema = schema\u0026#34;0 a int, 2 c double true, 3 d string\u0026#34; def schema(args: Any*): StructType = { val str = sc.s(args: _*).trim if (str.isEmpty) throw new IllegalArgumentException(\u0026#34;Empty Schema String\u0026#34;) val innerSeq = str split \u0026#34;,\u0026#34; map parseInnerStructField innerSeq.head match { case (None, _) =\u0026gt; StructType(innerSeq.map(_._2)) case (Some(_), _) =\u0026gt; val i2f = innerSeq.map({ case (oi, f) =\u0026gt; (oi.get, f) }).toMap val fSeq = (0 to i2f.keys.max).map(i =\u0026gt; i2f.getOrElse(i, StructField(s\u0026#34;fake_name_$i\u0026#34;, StringType))) StructType(fSeq) } } private def parseInnerStructField(str: String): InnerStructField = { assert(str != null \u0026amp;\u0026amp; !str.trim.isEmpty, \u0026#34;this is an empty string!\u0026#34;) val seq = str.trim.split(\u0026#34;\\\\s+\u0026#34;).toSeq Try(seq.head.toInt) match { case Success(index) =\u0026gt; (Some(index), parseStructField(seq.tail)) case Failure(_) =\u0026gt; (None, parseStructField(seq)) } } private def parseStructField(seq: Seq[String]): StructField = { seq match { case name +: typeStr +: Nil =\u0026gt; StructField(name, typeStr) case name +: typeStr +: nullable +: Nil if Try(nullable.toBoolean).isSuccess =\u0026gt; StructField(name, typeStr, nullable.toBoolean) case name +: typeStr +: nullable +: tail if Try(nullable.toBoolean).isSuccess =\u0026gt; StructField(name, typeStr, nullable.toBoolean).withComment(tail.mkString(\u0026#34; \u0026#34;)) case name +: typeStr +: tail =\u0026gt; StructField(name, typeStr).withComment(tail.mkString(\u0026#34; \u0026#34;)) case _ =\u0026gt; throw new IllegalArgumentException(s\u0026#34;not a valid struct field: \u0026#39;${seq.mkString(\u0026#34; \u0026#34;)}\u0026#39;\u0026#34;) } } private implicit def typeStrToDataType(str: String): DataType = { str match { case \u0026#34;bool\u0026#34; | \u0026#34;boolean\u0026#34; | \u0026#34;Boolean\u0026#34; =\u0026gt; DataTypes.BooleanType case \u0026#34;byte\u0026#34; | \u0026#34;Byte\u0026#34; =\u0026gt; DataTypes.ByteType case \u0026#34;short\u0026#34; | \u0026#34;Short\u0026#34; =\u0026gt; DataTypes.ShortType case \u0026#34;int\u0026#34; | \u0026#34;integer\u0026#34; | \u0026#34;Integer\u0026#34; =\u0026gt; DataTypes.IntegerType case \u0026#34;long\u0026#34; | \u0026#34;Long\u0026#34; =\u0026gt; DataTypes.LongType case \u0026#34;float\u0026#34; | \u0026#34;Float\u0026#34; =\u0026gt; DataTypes.FloatType case \u0026#34;double\u0026#34; | \u0026#34;Double\u0026#34; =\u0026gt; DataTypes.DoubleType case \u0026#34;decimal\u0026#34; | \u0026#34;bigdecimal\u0026#34; | \u0026#34;BigDecimal\u0026#34; =\u0026gt; DataTypes.createDecimalType() case \u0026#34;string\u0026#34; | \u0026#34;String\u0026#34; =\u0026gt; DataTypes.StringType case \u0026#34;timestamp\u0026#34; | \u0026#34;TimeStamp\u0026#34; =\u0026gt; DataTypes.TimestampType case \u0026#34;calendarinterval\u0026#34; | \u0026#34;CalendarInterval\u0026#34; =\u0026gt; DataTypes.CalendarIntervalType case \u0026#34;date\u0026#34; | \u0026#34;Date\u0026#34; =\u0026gt; DataTypes.DateType case \u0026#34;null\u0026#34; | \u0026#34;Null\u0026#34; =\u0026gt; DataTypes.NullType case \u0026#34;byte[]\u0026#34; | \u0026#34;Byte[]\u0026#34; | \u0026#34;Array[Byte]\u0026#34; =\u0026gt; DataTypes.BinaryType case _ if str.endsWith(\u0026#34;[]\u0026#34;) =\u0026gt; DataTypes.createArrayType(str.substring(0, str.length-2)) case _ if str.matches(\u0026#34;^Array\\\\[\\\\S+\\\\]$\u0026#34;) =\u0026gt; DataTypes.createArrayType(str.substring(6, str.length-1)) // never hit this case, because type string was splited by \u0026#39;,\u0026#39; above case _ if str.matches(\u0026#34;^Map\\\\[\\\\S+,\\\\S+\\\\]$\u0026#34;) || str.matches(\u0026#34;^Map\u0026lt;\\\\S+,\\\\S+\u0026gt;$\u0026#34;) =\u0026gt; val i = str.indexOf(\u0026#39;,\u0026#39;) val kt = str.substring(4, i) val vt = str.substring(i+1, str.length-1) DataTypes.createMapType(kt, vt) case _ =\u0026gt; throw new IllegalArgumentException(s\u0026#34;cannot resolve the type: \u0026#39;$str\u0026#39;!!!!\u0026#34;) } } } } 开发一个Spark程序 参考环境准备中IDEA小节创建项目，然后将起始代码复制进去\n注意构造SparkSession对象时master()方法接受的参数：如果是要在本地运行，使用\u0026quot;local[*]\u0026quot;；如果是链接Spark Standalone集群调试，使用形如\u0026quot;spark://hostname:port\u0026quot;的Spark Master URL（可以在Spark Master Web UI找到）；如果是要打成jar包放到公司的平台上跑，使用\u0026quot;yarn\u0026quot;参数\n选用自己使用顺手的组件进行功能实现和测试，一般现在本地环境以样例数据测试核心逻辑。然后按照部署目标的要求进行打包，部署到实际环境中进行测试。应当注意运行效率方面，有时候还需要进一步优化。\n开发常见问题 这里列出几个开发的时候比较常见的问题，此处列出的代码仅供演示，可能不完全正确\n数据过滤不严谨\n这种情况出现还算比较多，实际处理数据时应认真思考，尽可能筛掉异常数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 case class Student(id: String, name: String, age: Int) val students = sc.textFile(\u0026#34;/path/student-info.nb\u0026#34;) .map(_.split(\u0026#34;\\t\u0026#34;)) .map(a =\u0026gt; (a(0), a(1), a(2))) // 可能出现数组越界异常 .map(t =\u0026gt; Student(t._1, t._2, t._3.toInt)) // 可能出现转换失败异常 // 可以采用以下方式解决 val students = sc.textFile(\u0026#34;/path/student-info.nb\u0026#34;) .map(_.split(\u0026#34;\\t\u0026#34;)) .filter(_.length == 3) // 保证数组长度，消除数组越界异常 .map(a =\u0026gt; (a(0), a(1), a(2))) .filter(t =\u0026gt; Try(t._3.toInt).isSuccess) // 保证字段能正常转换 .map(t =\u0026gt; Student(t._1, t._2, t._3.toInt)) 以上演示了从数据格式上进行异常数据筛选，但某些数据从业务上也是不合法的，因此后续可能需要更多的filer和map进行数据筛选和变换\n过多数据放到一个集合/节点\n这种情况一般发生于使用Spark Shell进行快速数据分析时发生\n1 2 3 4 5 6 7 rdd.collect().foreach(println) // collect算子把rdd中的全部数据放到一个数组了，很容易就会挂掉，这需要我们自行保证rdd中的数据不多 // 可以先统计下数量，如果发现不多，那么可以这么做 rdd.count() // 也可以使用take rdd.take(20).foreach(println) 使用外部变量\n先看一个反例\n1 2 3 4 5 6 7 val names = new java.util.HashSet[String]() nameRdd.foreach(n =\u0026gt; names.Add(n)) // 使用foreach进行结果整理 someRdd.map(x =\u0026gt; {names.Add(x.name); x.other}) // map过程中“顺便”干点别的 // 或者这样 var sum = 0 intRdd.foreach(i =\u0026gt; sum += i) 基于我们熟悉的面向对象编程，已经养成了这么一个习惯：遍历一个集合，在遍历过程中提取我们需要的信息放到之前定义的变量里。\n在普通单线程Scala程序中，这么做虽然不推荐，但还是正确的；但是在Spark程序中，对RDD的操作如果这么干，是不正确的。要注意我们之前对RDD的并行化特点的介绍，Spark会把我们用λ表达的逻辑及其捕获的自由变量打包分发到每个节点，每个节点上的names sum与driver程序上的不再有任何关系，各节点的子任务运行完毕后，对变量的修改也不会反映到我们期望的变量身上，所以最终得到了错误的结果。\n这类问题没什么统一的解决方案，只能是具体问题具体看，算是在强制我们熟悉一下函数式编程。\n以上反例，可以这么改\n1 2 3 4 5 val names = nameRdd.distinct.collect // 用distinct算子去重 当然也得保证name数量没有太多 // map中不要“顺便”做任何事情了，逻辑分离开单独处理就好 val sum = intRdd.reduce(_ + _) // 用reduce算子求和 ","date":"2020-03-09T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/platform/spark/spark-dev-tutorial/3-spark-intro/","title":"3. 使用 Spark 进行数据分析"},{"content":"实际开发的注意事项 自研平台 版本 认证及资源隔离 项目部署 华为 版本 认证及资源隔离 项目部署 阿里云 浪潮 公司平台 1. 版本问题 NS平台基础JDK是1.7 Scala版本2.10.x Spark版本2.1.x 开发时应注意API版本\nSpark API应该还好，就我们目前的使用和开发方式，一般不太会撞上恰好2.1.x没有该API的情况，若要保证正常，可以下载spark-2.1.3-bin-hadoop2.6作为测试环境（感觉意义不大，因为Scala版本还是高于平台）\n注意对于Scala 和 Java的使用，需要换成适配版本进行开发和打包\n2. 认证及资源分离问题 在公司平台上启动任务需要注意指定用户、指定队列\n1 spark-shell --master yarn --principal xxx --keytab xxx.keytab --queue xxx 3. 项目部署 oozie被包装得妈都不认识了，得参考任务调度服务的开发文档。\n华为平台 华为平台是堡垒机操作，似乎防火墙只允许有限的几个机器访问运维界面(或者是因为集群在自己的子网中，在集群外根本找不到目标机器)，因此比较强烈依赖命令行操作\n操作华为集群要先加载环境（类似这样的命令）\n1 source bigdata-env 有时候环境中的用户身份还不具有操作集群的权限，还需要换个身份\n1 kinit -kt xxx.keytab xxx # ??? 1. 版本 印象中华为平台的基础JDK是1.8，Scala版本应该也不是2.10.x这么低，版本问题在华为集群上应该并不严重。印象中不知从哪听了一耳朵，有些地方华为的集群已经是Hadoop3.x，有些应用还需适配\n2. 认证及资源分离 华为集群对于动手启动的Spark Shell没有启用dynamic executor，因此启动一般分配有限的几个executor，任务没什么好并行的当然跑的慢了，所以要记得手动指定executor个数，如下所示\n注意：手动指定的executor个数必须是基于数据总量和具体任务进行考量，还应当看一下集群当前的状况，是不是资源特别紧张。不能武断地直接指定几百个executor跑自己的事情\n1 spark-shell --master yarn --num-executors 32 3. 部署 我记得是HUE界面上拖出来的进行部署的，也可以直接使用oozie进行定时/周期任务部署。华为集群上任务部署跟业界普遍做法比较一致，华为对这块没有进行特别深入的定制和包装\n阿里云 尚未交过学费，似乎变动炒鸡大，貌似只有一个点是这个平台\n浪潮 还没亲眼见过，听说直接就是原生的一套跑着\n","date":"2020-03-09T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/platform/spark/spark-dev-tutorial/4-real-dev-tips/","title":"4. 实际开发的注意事项"},{"content":"练手 以下各个问题基于最后附的数据，可以同时尝试下用SQL和RDD API来解决问题。\n配置环境，把本地Spark Shell跑起来\n能跑起来Spark Shell，说明环境配置正常。这是最基本的前提，可以使用IDEA直接进行本地调试\n过滤掉所有不正常数据\n按总成绩排名从高到底输出学号和姓名\n找出有不及格科目的同学\n分别找出各个科目的第一名\n分别找出男生和女生的第一名\n提供了几个解决思路，但是建议不要马上就看\n数据 将数据保存到本地，做成bcp文件\n各个字段分别是：学号，姓名，语文，数学，英语\n1 2 3 4 5 6 7 8 9 10 11 12 id-1\t学生-1\t43\t30\t0 id-2\t学生-2\t76\t83\t77 id-3\t学生-3\t98.5\t76\t90 id-4\t学生-4\t100\t79\t96 id-5\t学生-5\t59\t85\t76 id-6\t学生-6\t69\t78\t83 id-7\t学生-7\t75\t88\t69 id-8\t学生-8\t94\tabc\t79 id-9\t学生-9\t82\t91 id-10\t学生-10\t87\t68\t85 id-11\t84\t56\t95 学生-12\t99\t79\t60 字段是： 学号，性别\n1 2 3 4 5 6 7 8 9 10 11 12 id-1\t男 id-2\t女 id-3\t男 id-4\t女 id-5\t男 id-6\t女 id-7\t男 id-8\t女 id-9\t男 id-10\t女 id-11\t女 ","date":"2020-03-09T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/platform/spark/spark-dev-tutorial/5-exam/","title":"5. 小测试"},{"content":"几个解答思路 提供几个思路以解答前面提出的问题。\n其中理解门槛最低的是Spark SQL版，用SQL来描述业务逻辑，应该还是比较友好的。\n两个Spark Core版本的解决方案没有这么直观，两者都需要开发者熟悉常用算子。此外，使用基本数据结构的版本，需要把数据结构的变换过程理解吃透；定义业务类的版本理解稍容易些，搞清楚Scala中object、class、case class的概念应该就可以了。\n1. Spark SQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import org.apache.spark.sql.SparkSession object Application { def main(args : Array[String]) { val spark = SparkSession.builder().appName(\u0026#34;SparkApp\u0026#34;).master(\u0026#34;local[*]\u0026#34;).getOrCreate() import spark.implicits._ val scorebcp = \u0026#34;/mnt/c/Users/mxtao/Desktop/score.nb\u0026#34; val genderbcp = \u0026#34;/mnt/c/Users/mxtao/Desktop/gender.nb\u0026#34; // 加载数据 val scoreSchema = \u0026#34;id string, name string, yw float, sx float, yy float\u0026#34; spark.read.option(\u0026#34;sep\u0026#34;, \u0026#34;\\t\u0026#34;).schema(scoreSchema).csv(scorebcp).createOrReplaceTempView(\u0026#34;score\u0026#34;) val genderSchema = \u0026#34;id string, gender string\u0026#34; spark.read.option(\u0026#34;sep\u0026#34;, \u0026#34;\\t\u0026#34;).schema(genderSchema).csv(genderbcp).createOrReplaceTempView(\u0026#34;gender\u0026#34;) // 过滤无效数据，顺便求了总成绩放在这 spark.sql(\u0026#34;select *, (yw + sx + yy) as total from score where id is not null and name is not null and yw is not null and sx is not null and yy is not null\u0026#34;).createOrReplaceTempView(\u0026#34;score\u0026#34;) spark.sql(\u0026#34;select * from gender where id is not null and gender is not null\u0026#34;).createOrReplaceTempView(\u0026#34;gender\u0026#34;) // 按总成绩排名从高到底输出学号和姓名 spark.sql(\u0026#34;select id, name, total from score order by total desc\u0026#34;).show() // 找出有不及格科目的同学 spark.sql(\u0026#34;select * from score where yw \u0026lt; 60 or sx \u0026lt; 60 or yy \u0026lt; 60\u0026#34;).show() // 分别找出各个科目的第一名 spark.sql(\u0026#34;select * from score order by yw desc limit 1\u0026#34;).show() spark.sql(\u0026#34;select * from score order by sx desc limit 1\u0026#34;).show() spark.sql(\u0026#34;select * from score order by yy desc limit 1\u0026#34;).show() // 分别找出男生和女生的第一名 spark.sql(\u0026#34;select s.id, s.name, g.gender, s.total from score s join gender g on s.id == g.id\u0026#34;).createOrReplaceTempView(\u0026#34;gscore\u0026#34;) // 其实也可以不创建临时表，直接作为子查询也可 spark.sql(\u0026#34;select * from gscore where gender == \u0026#39;男\u0026#39; order by total desc limit 1\u0026#34;).show() spark.sql(\u0026#34;select * from gscore where gender == \u0026#39;女\u0026#39; order by total desc limit 1\u0026#34;).show() spark.stop() } } 2. Spark Core + 定义业务类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import org.apache.spark.sql.SparkSession import scala.util.Try // ----------- 定义业务类 ------------ object StudentScore { // 尝试从字符串构造业务对象 def tryApply(str: String): Option[StudentScore] = { if (verify(str)) Some(StudentScore(str)) else None } // 重载一个apply方法 def apply(str: String): StudentScore = { // require(verify(str), \u0026#34;not a correct string!\u0026#34;) val arr = str.split(\u0026#34;\\t\u0026#34;) StudentScore(arr(0), arr(1), arr(2).toFloat, arr(3).toFloat, arr(4).toFloat) } // 验证外部输入的字符串或者数组是否合法 def verify(str: String): Boolean = str!=null \u0026amp;\u0026amp; str.nonEmpty \u0026amp;\u0026amp; verify(str.split(\u0026#34;\\t\u0026#34;)) def verify(arr: Array[String]): Boolean = arr!=null \u0026amp;\u0026amp; arr.length==5 \u0026amp;\u0026amp; arr.forall(_.nonEmpty) \u0026amp;\u0026amp; arr.slice(2, 5).forall(s =\u0026gt; Try(s.toFloat).isSuccess) } // 注意，这里是一个 case class，要理解清楚它与普通 class 有何不同 case class StudentScore(val id: String, val name: String, val yw: Float, val sx: Float, val yy: Float){ def total: Float = yw + sx + yy def isPassed: Boolean = yw \u0026gt;= 60.0 \u0026amp;\u0026amp; sx \u0026gt;= 60.0 \u0026amp;\u0026amp; yy \u0026gt;= 60.0 def notPassed: Boolean = !isPassed } // ----------- 主程序本体 -------------- object Application { def main(args : Array[String]) { val spark = SparkSession.builder().appName(\u0026#34;SparkApp\u0026#34;).master(\u0026#34;local[*]\u0026#34;).getOrCreate() val sc = spark.sparkContext import spark.implicits._ val scorebcp = \u0026#34;/mnt/c/Users/mxtao/Desktop/score.nb\u0026#34; val genderbcp = \u0026#34;/mnt/c/Users/mxtao/Desktop/gender.nb\u0026#34; val score = sc.textFile(scorebcp) .map(s =\u0026gt; StudentScore.tryApply(s)) // 尝试从字符串构造对象 .filter(_.nonEmpty) // 把构造失败的过滤掉 .map(_.get) // 取得对象本体，此时得到RDD[StudentScore] // 也可以用另一种处理办法 // val score = sc.textFile(scorebcp) // .filter(s =\u0026gt; StudentScore.verify(s)) // 验证字符串是否正确，把不正确的字符串丢掉 // .map(s =\u0026gt; StudentScore(s)) // 从字符串构造业务对象 val gender = sc.textFile(genderbcp) .map(_.split(\u0026#34;\\t\u0026#34;)) .filter(_.length == 2) .map(a =\u0026gt; (a(0), a(1))) .filter(t =\u0026gt; t._1.nonEmpty \u0026amp;\u0026amp; t._2.nonEmpty) // 按总成绩排名从高到底输出学号和姓名 score.sortBy(_.total, ascending = false) .map(s =\u0026gt; s\u0026#34;${s.id}\\t${s.name}\u0026#34;) .collect() .foreach(println) // 找出有不及格科目的同学 score.filter(_.notPassed).collect.foreach(println) // 分别找出各个科目的第一名 score.sortBy(_.yw, ascending = false).take(1).foreach(print) score.sortBy(_.sx, ascending = false).take(1).foreach(print) score.sortBy(_.yy, ascending = false).take(1).foreach(print) // 分别找出男生和女生的第一名 val score2 = score.map(s =\u0026gt; (s.id, s)).join(gender).map(_._2) score2.filter(_._2 == \u0026#34;男\u0026#34;).map(_._1).sortBy(_.total, ascending = false).take(1).foreach(println) score2.filter(_._2 == \u0026#34;女\u0026#34;).map(_._1).sortBy(_.total, ascending = false).take(1).foreach(println) spark.close() } } 3. Spark Core + 基本数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import org.apache.spark.sql.SparkSession import scala.util.Try object Application { def main(args : Array[String]) { val spark = SparkSession.builder().appName(\u0026#34;SparkApp\u0026#34;).master(\u0026#34;local[*]\u0026#34;).getOrCreate() val sc = spark.sparkContext import spark.implicits._ val scorebcp = \u0026#34;/mnt/c/Users/mxtao/Desktop/score.nb\u0026#34; val genderbcp = \u0026#34;/mnt/c/Users/mxtao/Desktop/gender.nb\u0026#34; // 读取文件，过滤无效数据 val score = sc.textFile(scorebcp) .map(_.split(\u0026#34;\\t\u0026#34;)) .filter(_.length == 5) // 过滤掉字段有缺失的数据 .map(a =\u0026gt; (a(0), a(1), a(2), a(3), a(4))) // 把数组转换成元组 .filter(t =\u0026gt; t._1.nonEmpty \u0026amp;\u0026amp; t._2.nonEmpty \u0026amp;\u0026amp; t._3.nonEmpty \u0026amp;\u0026amp; t._4.nonEmpty \u0026amp;\u0026amp; t._5.nonEmpty) // 保证必要字段不为空 .filter(t =\u0026gt; Try(t._3.toFloat).isSuccess \u0026amp;\u0026amp; Try(t._4.toFloat).isSuccess \u0026amp;\u0026amp; Try(t._5.toFloat).isSuccess) // 保证成绩字段是合法的 .map(t =\u0026gt; (t._1, t._2, t._3.toFloat, t._4.toFloat, t._5.toFloat)) // 转换成 (学号, 姓名, 语文, 数学, 英语) 形式 val gender = sc.textFile(genderbcp) .map(_.split(\u0026#34;\\t\u0026#34;)) .filter(_.length == 2) .map(a =\u0026gt; (a(0), a(1))) .filter(t =\u0026gt; t._1.nonEmpty \u0026amp;\u0026amp; t._2.nonEmpty) // 按总成绩排名从高到底输出学号和姓名 score.map(t =\u0026gt; (t._1, t._2, t._3 + t._4 + t._5)) // 转换成 (学号, 姓名, 总成绩) 形式 .sortBy(_._3, ascending = false) // 以总成绩为key，按从低到高排序 .map(t =\u0026gt; s\u0026#34;${t._1}\\t${t._2}\u0026#34;) // 格式化数据，为输出做准备 .collect() // 将分布在各个节点的数据拿到本地 .foreach(println) // 打印到控制台 // 找出有不及格科目的同学 score.filter(t =\u0026gt; t._3 \u0026lt; 60.0 || t._4 \u0026lt; 60.0 || t._5 \u0026lt; 60.0) .collect() .foreach(println) // 分别找出各个科目的第一名 score.sortBy(_._3, ascending = false) .take(1) .foreach(println) score.sortBy(_._4, ascending = false) .take(1) .foreach(println) score.sortBy(_._5, ascending = false) .take(1) .foreach(println) // 分别找出男生和女生的第一名 val score2 = score.map(t =\u0026gt; (t._1, (t._1, t._2, t._3 + t._4 + t._5))) // 转换成二元组，为join做准备，此处转换成了 (学号, (学号, 姓名, 总成绩)) 形式 .join(gender) // 与gender数据集(结构为：(学号,性别))关联碰撞，策略为丢弃所有未关联数据，结果结构为: (学号, ((学号, 姓名, 总成绩), 性别)) .map(_._2) // 丢掉不再使用的key，然后结构变为：((学号, 姓名, 总成绩), 性别) score2.filter(_._2 == \u0026#34;男\u0026#34;) .map(_._1) .sortBy(_._3, ascending = false) .take(1) .foreach(println) score2.filter(_._2 == \u0026#34;女\u0026#34;) .map(_._1) .sortBy(_._3, ascending = false) .take(1) .foreach(println) spark.close() } } ","date":"2020-03-09T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/platform/spark/spark-dev-tutorial/6-answer/","title":"6. 小测试参考思路"},{"content":"spark-dev-tour Spark Application Development Tour\n大纲\n下方链接可能失效，可尝试系列目录\n培训附件\n环境准备\nWindows JDK Scala SDK Spark IDEA WSL / Linux VSCode Remote Scala Language\nScala编程语言入门\nSpark开发\nSpark简介 Spark处理数据 Spark Core Spark SQL 开发一个Spark程序 开发常见问题 实际开发的注意事项\n主要公司自研平台和华为的平台\n练手\n几个练习\nSpark Release 3.0.0\n","date":"2020-03-09T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/platform/spark/spark-dev-tutorial/0-intro/","title":"Spark开发入门-介绍"},{"content":"F#-Notes-2 Imperative Programming F#不仅可以用于纯函数式编程，也可用于命令式编程。其实对于某些问题，命令式编程是一个十分行之有效的做法。\nunit Type 对于某些方法，它不需要传入参数，也不会返回任何值，传入的参数和和返回值类型都是unit，有点类似C#中的void和CLR中的System.Void。对于函数式编程来说，如果一个函数没有接受值也没有返回值，那么它什么也没做；但是对于命令式编程，我们需要意识到副作用的存在，所以这类函数依旧有其作用的。unit是字面量()的类型，因此如果一个函数不接受值，不返回值，那么只需要在参数处和返回值处放上这它就好了。如下所示\n1 2 3 let aFunction () = () // val: aFunction: unit -\u0026gt; unit aFunction () 也可以显式忽略某个函数的值\n1 aFunc() |\u0026gt; ignore The mutable Keyword 1 2 let mutable word = \u0026#34;Hello\u0026#34; word \u0026lt;- \u0026#34;World\u0026#34; // type should be same Defining Mutable Records 1 2 3 type MutableRecord = {first: string; mutable second: string} let tem = {first= \u0026#34;\u0026#34;; second=\u0026#34;\u0026#34;} tem.second \u0026lt;- \u0026#34;\u0026#34; The Reference Type F# 4.0 之前不能直接将mutable的值捕获到闭包里面\n! 取值 := 赋值 1 2 3 4 5 6 7 8 9 10 11 12 13 let totalArray () = // define an array literal let array = [| 1; 2; 3 |] // define a counter let total = ref 0 // loop over the array for x in array do // keep a running total total := !total + x // print the total printfn \u0026#34;total: %i\u0026#34; !total totalArray() Array 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // define an array literal let rhymeArray = [| \u0026#34;Went to market\u0026#34;; \u0026#34;Stayed home\u0026#34;; \u0026#34;Had roast beef\u0026#34;; \u0026#34;Had none\u0026#34; |] // unpack the array into identifiers let firstPiggy = rhymeArray.[0] let secondPiggy = rhymeArray.[1] let thirdPiggy = rhymeArray.[2] let fourthPiggy = rhymeArray.[3] // update elements of the array rhymeArray.[0] \u0026lt;- \u0026#34;Wee,\u0026#34; rhymeArray.[1] \u0026lt;- \u0026#34;wee,\u0026#34; rhymeArray.[2] \u0026lt;- \u0026#34;wee,\u0026#34; rhymeArray.[3] \u0026lt;- \u0026#34;all the way home\u0026#34; 1 2 3 4 5 6 7 8 9 10 // 交错数组 // define a jagged array literal let jagged = [| [| \u0026#34;one\u0026#34; |] ; [| \u0026#34;two\u0026#34; ; \u0026#34;three\u0026#34; |] |] // unpack elements from the arrays let singleDim = jagged.[0] let itemOne = singleDim.[0] let itemTwo = jagged.[1].[0] // print some of the unpacked elements printfn \u0026#34;%s %s\u0026#34; itemOne itemTwo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 数组初始化 // an array of characters let chars = [| \u0026#39;1\u0026#39; .. \u0026#39;9\u0026#39; |] // an array of tuples of number, square let squares = [| for x in 1 .. 9 -\u0026gt; x, x*x |] // print out both arrays printfn \u0026#34;%A\u0026#34; chars printfn \u0026#34;%A\u0026#34; squares // 数组切片 // 切片语法也可用于List let arr = [|1; 3; 5; 7; 11; 13|] let middle = arr.[1..4] // [|3; 5; 7; 11|] let start = arr.[..3] // [|1; 3; 5; 7|] let tail = arr.[1..] // [|3; 5; 7; 11; 13|] let ocean = Array2D.create 100 100 0 // Create a ship: for i in 3..6 do ocean.[i, 5] \u0026lt;- 1 // Pull out an area hit by a \u0026#39;shell\u0026#39;: let hitArea = ocean.[2..5, 2..5] // We can see a rectangular area by \u0026#39;radar\u0026#39;: let radarArea = ocean.[3..4, *] Control Flow 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 if x = 1 then printfn \u0026#34;eq\u0026#34; else printfn \u0026#34;ne\u0026#34; // an array for words let words = [| \u0026#34;Red\u0026#34;; \u0026#34;Lorry\u0026#34;; \u0026#34;Yellow\u0026#34;; \u0026#34;Lorry\u0026#34; |] // use a for loop to print each element for word in words do printfn \u0026#34;%s\u0026#34; word for index = 0 to Array.length words - 1 do printfn \u0026#34;%s\u0026#34; words.[index] for index = Array.length words downto 0 do printfn \u0026#34;%s\u0026#34; words.[index] let mutable tem = 10 while (tem \u0026gt; 0) do tem \u0026lt;- tem - 1 // Count upwards: for i in 0..10 do printfn \u0026#34;%i green bottles\u0026#34; i // Count downwards: for i in 10..-1..0 do printfn \u0026#34;%i green bottles\u0026#34; i // Count upwards in tens for i in 0..10..100 do printfn \u0026#34;%i green bottles\u0026#34; i Calling Static Methods and Properties from .NET Libraries 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // calling static method ClassName.MethodName(arg1, arg2, arg3) // or in a curried way let methodName arg1 arg2 arg3 = ClassName.MethodName(arg1, arg2, arg3) let temMethod arg1 = methodName arg1 // named paramaters open System.IO // open a file using named arguments let file = File.Open(path = \u0026#34;test.txt\u0026#34;, mode = FileMode.Append, access = FileAccess.Write, share = FileShare.None) // close it! file.Close() Using Objects and Instance Members from .NET Libraries 1 2 3 4 5 6 7 8 9 10 open System.IO // file name to test let filename = \u0026#34;test.txt\u0026#34; // bind file to an option type, depending on whether // the file exist or not let file = if File.Exists(filename) then Some(new FileInfo(filename, Attributes = FileAttributes.ReadOnly)) else None 1 2 3 4 5 6 7 8 9 open System // how to wrap a method that take a delegate with an F# function // the \u0026lt;_\u0026gt; means don\u0026#39;t care the type of the Predicate\u0026#39;s argument let findIndex f arr = Array.FindIndex(arr, new Predicate\u0026lt;_\u0026gt;(f)) // define an array literal let rhyme = [| \u0026#34;The\u0026#34;; \u0026#34;cat\u0026#34;; \u0026#34;sat\u0026#34;; \u0026#34;on\u0026#34;; \u0026#34;the\u0026#34;; \u0026#34;mat\u0026#34; |] // print index of the first word ending in \u0026#39;at\u0026#39; printfn \u0026#34;First word ending in \u0026#39;at\u0026#39; in the array: %i\u0026#34; (rhyme |\u0026gt; findIndex (fun w -\u0026gt; w.EndsWith(\u0026#34;at\u0026#34;))) Using Indexers from .NET Libraries 1 2 3 4 5 6 7 8 9 10 11 open System.Collections.Generic // create a ResizeArray let stringList = let temp = new ResizeArray\u0026lt;string\u0026gt;() temp.AddRange([| \u0026#34;one\u0026#34;; \u0026#34;two\u0026#34;; \u0026#34;three\u0026#34; |]); temp // unpack items from the resize array let itemOne = stringList.Item(0) let itemTwo = stringList.[1] // print the unpacked items printfn \u0026#34;%s %s\u0026#34; itemOne itemTwo Working with Events from .NET Libraries 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 open System.Timers let timedMessages() = // define the timer let timer = new Timer(Interval = 3000.0, Enabled = true) // a counter to hold the current message let mutable messageNo = 0 // the messages to be shown let messages = [ \u0026#34;bet\u0026#34;; \u0026#34;this\u0026#34;; \u0026#34;gets\u0026#34;; \u0026#34;really\u0026#34;; \u0026#34;annoying\u0026#34;; \u0026#34;very\u0026#34;; \u0026#34;quickly\u0026#34; ] // add an event to the timer timer.Elapsed.Add(fun _ -\u0026gt; // print a message printfn \u0026#34;%s\u0026#34; messages.[messageNo] messageNo \u0026lt;- messageNo + 1 if messageNo = messages.Length then timer.Enabled \u0026lt;- false) timedMessages() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 open System.Timers let timedMessagesViaDelegate() = // define the timer let timer = new Timer(Interval = 3000.0, Enabled = true) // a counter to hold the current message number let mutable messageNo = 0 // the messages to be shown let messages = [ \u0026#34;bet\u0026#34;; \u0026#34;this\u0026#34;; \u0026#34;gets\u0026#34;; \u0026#34;really\u0026#34;; \u0026#34;annoying\u0026#34;; \u0026#34;very\u0026#34;; \u0026#34;quickly\u0026#34; ] // function to print a message let printMessage = fun _ _ -\u0026gt; // print a message printfn \u0026#34;%s\u0026#34; messages.[messageNo] messageNo \u0026lt;- (messageNo + 1) % messages.Length // wrap the function in a delegate let del = new ElapsedEventHandler(printMessage) // add the delegate to the timer timer.Elapsed.AddHandler(del) |\u0026gt; ignore // return the time and the delegate so we can // remove one from the other later (timer, del) // Run this first: let timer, del = timedMessagesViaDelegate() // Run this later: timer.Elapsed.RemoveHandler(del) Pattern Matching with .NET Types 1 2 3 4 5 6 7 8 9 10 11 12 13 // a list of objects let simpleList = [ box 1; box 2.0; box \u0026#34;three\u0026#34; ] // a function that pattern matches over the // type of the object it is passed let recognizeType (item : obj) = match item with | :? System.Int32 -\u0026gt; printfn \u0026#34;An integer\u0026#34; | :? System.Double -\u0026gt; printfn \u0026#34;A double\u0026#34; | :? System.String -\u0026gt; printfn \u0026#34;A string\u0026#34; | _ -\u0026gt; printfn \u0026#34;Unknown type\u0026#34; // iterate over the list of objects List.iter recognizeType simpleList 1 2 3 4 5 6 7 8 9 10 11 12 // list of objects let anotherList = [ box \u0026#34;one\u0026#34;; box 2; box 3.0 ] // pattern match and print value let recognizeAndPrintType (item : obj) = match item with | :? System.Int32 as x -\u0026gt; printfn \u0026#34;An integer: %i\u0026#34; x | :? System.Double as x -\u0026gt; printfn \u0026#34;A double: %f\u0026#34; x | :? System.String as x -\u0026gt; printfn \u0026#34;A string: %s\u0026#34; x | x -\u0026gt; printfn \u0026#34;An object: %A\u0026#34; x // iterate over the list pattern matching each item List.iter recognizeAndPrintType anotherList 1 2 3 4 5 6 7 8 9 10 11 12 13 14 try // look at current time and raise an exception // based on whether the second is a multiple of 3 if System.DateTime.Now.Second % 3 = 0 then raise (new System.Exception()) else raise (new System.ApplicationException()) with | :? System.ApplicationException -\u0026gt; // this will handle \u0026#34;ApplicationException\u0026#34; case printfn \u0026#34;A second that was not a multiple of 3\u0026#34; | _ -\u0026gt; // this will handle all other exceptions printfn \u0026#34;A second that was a multiple of 3\u0026#34; The |\u0026gt; Operator 1 2 // the definition of |\u0026gt; let (|\u0026gt;) x f = f x 1 2 3 4 5 6 7 8 9 10 11 // grab a list of all methods in memory let methods = System.AppDomain.CurrentDomain.GetAssemblies() |\u0026gt; List.ofArray |\u0026gt; List.map ( fun assm -\u0026gt; assm.GetTypes() ) |\u0026gt; Array.concat |\u0026gt; List.ofArray |\u0026gt; List.map ( fun t -\u0026gt; t.GetMethods() ) |\u0026gt; Array.concat // print the list printfn \u0026#34;%A\u0026#34; methods ","date":"2017-10-16T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/language/fsharp/fsharp-note-2/","title":"F#-Notes-2 Imperative Programming"},{"content":"Quartz Tutorial 10 - Advanced (Enterprise) Features 集群 目前JDBC-JobStore（JobStoreTX/JobStoreCMT）和TerracottaJobStore可以集群式工作。高级功能包括负载均衡和作业的fail-over（这个需要JobDetail的request recovery标志设置为true）。\n通过设置org.quartz.jobStore.isClustered属性为true，JobStoreTX或JobStoreCMT就能集群式运行了。对于集群的每个实例，它们应该使用quartz.properties配置文件的同一份拷贝，这是为了保证一致性，当然有些配置项可以是机器独立的。线程池大小和org.quartz.scheduler.instanceId属性可以不同。集群的每个节点必须是独立的实例ID，这其实很简单，只需要在这个属性上填写AUTO即可。\n集群内节点的时间必须是同步的（从同一个时间同步服务获取时间），此外还应当常规运行同步来保证节点间时间误差不超过1秒\n不要针对任何其他实例运行的相同的一组表来启动非群集实例。 不然数据会被严重损坏，而且会出现不正常的行为。\n对于每次触发，只有一个节点会执行作业。例如：假定一个触发器设置的是每10秒触发一次，12:00:00的时刻确实有个节点执行作业了，那么12:00:10的时候也确实会有一个节点会执行作业。而且每次也不一定是同一个节点，会随机选择节点来执行作业的。\n使用TerracottaJobStore的集群只需要简单配置一下调度器去使用它即可，然后你的调度器将使用集群全部机器。\n您可能还需要考虑如何设置Terracotta服务器，特别是启用一些特别的配置选项，例如在磁盘上存储数据，使用fsync以及运行Terracotta服务器阵列。\n关于JobStore和Terracotta的更多信息请查看Quartz Scheduler | Terracotta\nJTA Transactions 之前在JobStore一节介绍过，JobStoreCMT允许Quartz调度操作放在一个大的JTA事务中。\n只要设置了org.quartz.scheduler.wrapJobExecutionInUserTransaction属性为true，作业也可以在一个JTA事务(UserTransaction)中执行。这种情况下，一个JTA事务将会在作业的execute方法调用之前begin()，在方法执行完毕之后commit()。\n除了Quartz自动将作业的执行包装到JTA事务中，在使用JobStoreCMT时也可以使用Scheduler接口参与事务处理。只要保证了你在调度器上调用一个方法前启动了一个事务。还可以更直接点，可以使用UserTransaction，或将使用调度器的代码放在使用容器管理事务的SessionBean中。\n","date":"2017-09-14T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/10.advanced-features/","title":"Quartz Tutorial 10 - Advanced (Enterprise) Features"},{"content":"Quartz Tutorial 11 - Miscellaneous Features of Quartz Plug-Ins Quartz提供了一个接口(org.quartz.spi.SchedulerPlugin) 用于插入附加的功能.\n与Quartz一同发布的，提供了各种实用功能的插件可以在org.quartz.plugins包中找到。 他们提供的功能有：在调度器启动时自动调度作业，记录作业和触发事件的日志，并确保当JVM退出时完全关闭调度器。\nJobFactory 当触发器触发时，将使用调度器配置的JobFactory实例化与之关联的作业。默认的JobFactory只是在作业类上调用newInstance()方法。你也可能需要提供自己的JobFactory实现，让应用程序的IoC或DI容器来做生成/初始化作业实例之类的操作。\n查看org.quartz.spi.JobFactory接口，以及与之有关的Scheduler.setJobFactory(factory)方法。\n‘Factory-Shipped’ Jobs Quartz也提供了一些很实用的作业类，例如发送邮件、调用EJB等等。这些开箱即用的作业类可以在org.quartz.jobs包中找到。\n","date":"2017-09-14T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/11.miscellaneous-features/","title":"Quartz Tutorial 11 - Miscellaneous Features of Quartz"},{"content":"Quartz Tutorial 8 - Job Stores JobStore用于保持对你交给调度器的“工作数据”的持续跟踪，这些数据包括：作业、触发器、日历等等。选择合适的JobStore对你的Quartz调度器实例来说，是相当重要的一步。当然，你了解了他们之间的差异之后做出选择还是很简单的。你需要通过属性文件（或对象）告知SchedulerFactory你需要用到的JobStore（及对它的相关配置），然后SchedulerFactory生成一个调度器实例。\n绝不要在代码中直接使用JobStore实例，虽然很多人出于各种各样的原因已经尝试过了。JobStore是Quartz在后台自己使用的。你只需要告知Quartz（通过配置信息）你要用到哪个JobStore，然后你只需要在代码中使用Scheduler接口就好。\nRAMJobStore RAMJobStore是最简单的JobStore了，它也是性能最好的了（在CPU时间方面）。RAMJobStore将它所有的数据都放在内存里，也因此而得名。这也是为何它为何如此轻量快速，也最容易配置。但缺点是，一旦你的应用结束（或者崩溃），所有调度相关信息都没了，这也意味着RAMJobStore不能赋予它的作业和触发器“非易失性”。对有些程序来说这不是什么问题，甚至是期望行为，但对有些程序来说，这完全不可接受。\n要使用RAMJobStore（假定你正在用StdSchedulerFactory），你只需要将你用于配置Quartz处将JobStore类属性设置为org.quartz.simpl.RAMJobStore，如下所示\n1 org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore 好了，除此之外没有任何需要配置的地方了。\nJDBCJobStore 顾名思义，JDBCJobStore将它所有的数据存储到了数据库里，然后通过JDBC访问。因此相对于RAMJobStore，对它的配置就有些麻烦，当然也没有那么快了。然而它的性能倒也没有特别坏，尤其是你在数据库表中在主键上建立了索引。在正常的现代集群中，调度器和数据库之间链接正常，那么获取和更新一个正在触发的触发器状态的时间一般小于10毫秒。\nJDBCJobStore几乎可以和所有数据库一起使用，例如Oracle、PostgreSQL、MySQL、MS SQL Server、 HSQLDB和DB2。要使用JDBCJobStore，你首先要在数据库中创建Quartz要用的那些表。在Quartz分支的docs/dbTables文件夹下你能找到建表的SQL脚本。如果里面没有用于你的数据库类型的脚本，你只需要打开任何一个，然后改改。有一点要特别提出来，所有以QRTZ_开头的表名（例如QRTZ_TRIGGERS QRTZ_JOB_DETAIL），这些前缀实际上可以是任意的。只要你在Quartz的配置文件里告知了JDBCJobStore你用了啥前缀就好。当你想在一个数据库中为不同的调度器实例创建不同的表的时候，这个特性还是很有用的。\n表建完了之后，在配置和启动JDBCJobStore之前你还有个“重大决定”需要做出：你需要使用哪种事务。如果你不需要将调度命令（例如添加、移除触发器）束缚于于其它事务，那么你可以让Quartz用JobStoreTX作为你的JobStore来管理事务，这也是大部分人的选择。\n如果你需要让Quartz与其他事务一起工作（例如在一个J2EE应用服务器内部），那你应该用JobStoreCMT，这样Quartz将会让应用服务器容器来管理事务。\n最后一部分就是设置数据源，这样JDBCJobStore就能获取与数据库的连接了。数据源是在Quartz的配置文件里定义的。定义方式有多种，一种是让Quartz自己创建和管理-直接提供所有的连接信息。另一种是让Quartz使用它所在的应用服务器提供的数据源来干活-告知JDBCJobStore数据源的JDNI名字。要想知道配置文件的更多细节，去看看配置文件的例子，它在docs/config文件夹下。\n假定你在使用StdSchedulerFactory，首先要在配置文件里将JobStore类属性写成org.quartz.impl.jdbcjobstore.JobStoreTX或者org.quartz.impl.jdbcjobstore.JobStoreCMT，这取决于你之前怎样决定的事务处理方式。\n1 org.quartz.jobStore.class = org.quartz.impl.jbdcjobstore.JobStoreTX 接下来，你要选择一个驱动委托(DriverDelegate)给JobStore用。驱动委托是负责处理所有JDBC相关的工作，而且这也取决于你用什么数据库了。StdJDBCDelegate是使用“vanilla”JDBC代码（和SQL语句）来干活的委托。如果你没有配置这一项，那么Quartz就会使用这个。我们只为使用StdJDBCDelegate会出问题的数据库提供了专用委托（还真不少）。那些委托能够在org.quartz.imple.jdbcjobstore包或者它的子包发现。这些专用委托包括DB2v6Delegate(DB2 version6 及之前版本)、HSQLDBDelegate(用于 HSQLDB), MSSQLDelegate (用于 Microsoft SQLServer), PostgreSQLDelegate (用于 PostgreSQL), WeblogicDelegate (用于Weblogic提供的JDBC驱动), OracleDelegate (用于Oracle), 以及其他。\n选定了你的委托之后，将它的类名配置给JDBCJobStore使用\n1 org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate 接下来告知JobStore你使用的表名前缀\n1 org.quartz.jobStore.tablePrefix = QRTZ_ 最后你需要告知JobStore你要是用的数据源。命名的数据源必须也在Quartz配置文件里定义过了，在这个例子中，我们只写了个数据源的名字，但是对这个名字的定义也在配置文件里的某处。\n1 org.quartz.jobStore.dataSource = myDS 如果你的调度器总是很忙（例如：总是有线程池线程数量个的作业在执行），那么此时你就应该考虑将线程池线程数+2，让它有余力保持对数据源的链接\norg.quartz.jobStore.useProperties配置参数可以设置成true（默认是false）来告知JobStore在JobDataMap中的所有的值都是String类型，这样就能存成简单的名字-值对，而不是从反序列化来的更复杂的对象。当数据很长的时候，这样子也很安全，此外还避免了在序列化非字符串类的时候可能存在的版本问题。\nTerracottaJobStore TerracottaJobStore是Quartz 1.7新出的。它提供了不使用数据库但依然保持伸缩性和鲁棒性的手段。这意味着你可以将这些数据存储到应用的其他部分而不是放到数据库里。\nTerracottaJobStore可以集群式运行也可以单独运行。它可以持久保存你的作业数据不管应用是不是发生了重启、崩溃等，因为数据存储在了Terracotta服务器上。它的性能比通过JDBCJobStore访问数据库好多了（好一个数量级），但也比RAMJobStore慢多了。\n要使用TerracottaJobStore(假定你在使用StdSchedulerFactory)，你只需要在配置文件中写明类型，然后给出Terracotta服务器的地址就好了\n1 2 org.quartz.jobStore.class = org.terracotta.quartz.TerracottaJobStore org.quartz.jobStore.tcConfigUrl = localhost:9510 关于JobStore和Terracotta的更多信息可以查看 Quartz Scheduler | Terracotta\n","date":"2017-09-14T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/8.job-store/","title":"Quartz Tutorial 8 - Job Stores"},{"content":"Quartz Tutorial 9 - Configuration, Resource Usage and SchedulerFactory Quartz是模块化的架构，因此它运行的时候必定是多个组件互相协作运行的。在Quartz能干活之前主要有如下几个组件需要配置：\nThreadPool JobStore DataSources（如果有必要） Scheduler ThreadPool为Quartz提供了一些可用于执行作业的线程。线程池里的线程数越多，能同时执行的作业也就越多。不过，过多的线程可能会拖累你的系统。大部分Quartz的用户发现5个左右的线程就很充足-因为在任何给定时间，程序的作业都远小于100个，而且也一般不会同时运行，此外作业也一般是短暂存在的（因为完成的很快）。有些用户就发觉他们需要10个、20个、50个甚至100个线程-因为有成百上千个触发器需要调取，而且在任意时刻平均有10到100个作业需要执行。因此可见，你的调度器到底需要多大的线程池完全取决于你要什么。这里没有确定的数字，当然要让这个数字尽可能小（为了尽可能少占用你机器的资源）-但是你也要保证这个数目是够用的。要注意：如果一个触发器到了触发时间了，但是没有闲置线程让它干活，Quartz将会暂停它直到有了可用的县城资源，然后作业才能执行-可能会晚个多少毫秒吧。如果在调度器配置的“misfire阈值”内一直没有可用线程，那么就会引发misfire。\nThreadPool接口定义在org.quartz.spi包中，你可以通过任何方式提供该接口的一个实现。Quartz提供了一个简单（但是非常靠谱）的线程池实现，org.quartz.simpl.SimpleThreadPool。这个线程池内部维护了一个定长线程池-不会扩展也不会收缩-但是相当健壮经过了相当严密的测试，几乎每个用户都在用Quartz提供的这个线程池。\nJobStore和DataSource刚刚讨论过了。这里值得注意的是，所有的JobStore都实现了org.quartz.spi.JobStore接口，那么如果现有的实现满足不了你的需求，自己实现一个。\n最后，你需要创建一个调度器实例。调度器自身需要一个名字，告知它的RMI设置，然后给一个JobStore和ThreadPool的实例。RMI设置包括调度器是否将自己创建成一个用于RMI的服务对象（可以被远程连接），用什么主机名和端口等等。\nStdSchedulerFactory也可以生成实际上是远程进程创建的调度器实例代理的调度器实例。\nStdSchedulerFactory StdSchedulerFactory是org.quartz.SchedulerFactory接口的一个实现。它使用了一系列属性(java.util.Properties)来创建和初始化Quartz调度器。这些属性一般存储在文件中，但是你也可以直接在程序中创建然后交给工厂类。简单调用一下getScheduler()方法，工厂类就会实例化一个对象，然后将之初始化（包括线程池、JobStore和数据源），然后将它的公共接口返回给外界。\n在Quartz分支的docs/config目录下有些简单的配置（包括对属性的描述）。在Quartz文档的Reference目录下，手册里的Configuration小节有完整的文档。\nDirectSchedulerFactory DirectorySchedulerFactory是SchedulerFactory接口的另一个实现。当你想用编程风格创建一个调度器实例的时候，这就很有用了。但这个一般不鼓励使用，出于以下两个原因：\n它需要用户对于正在做什么有非常深的理解 它不支持声明式的配置-换句话说，你必须完全动手用编码的方式写出调度器的所有设置 Logging 对于所有的日志需求，Quartz使用SLF4J框架。如果你需要调整日志设置（例如调整日志的输出以及它们输出的位置），你需要了解这个框架，但是这个不在本文档的范围内了。\n如果你需要捕获更多关于触发器触发和作业执行的信息，你可能需要去了解一下org.quartz.plugins.history.LoggingJobHistoryPlugin和/或org.quartz.plugins.history.LoggingTriggerHistoryPlugin。\n","date":"2017-09-14T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/9.configuration-resourceusage-schedulerfactory/","title":"Quartz Tutorial 9 - Configuration, Resource Usage and SchedulerFactory"},{"content":"Quartz Tutorial Cron Expression 这里的东西已经在 CronTrigger 一节介绍过了，这里没有照常翻译原文，只记录一下概要。\n格式 子表达式名 是否必需 定义域 可用的特殊符号 Seconds YES 0-59 , - * / Minutes YES 0-59 , - * / Hours YES 0-23 , - * / Day of month YES 1-31 , - * ? / L W Month YES 1-12 or JAN-DEC , - * / Day of week YES 1-7 or SUN-SAT , - * ? / L # Year NO empt, 1970-2099 , - * / 特殊符号 * 所有值 ? 不确定的值 - 用于划定一个范围 / 用于声明增量 , 分割多个值 L 月份的最后一天或者周六，此时的L-2表示倒数第二 W 给定日期最近的工作日 周一到周五 LW表示月份中最后的工作日 # 月份中的第几个周几 例如6#3 第三个周五(6=FRI) 例子 表达式 意义 0 0 12 * * ? 每天中午12点 0 15 10 ? * * 每天上午10:15 0 15 10 * * ? 每天上午10：15 0 15 10 * * ? * 每天上午10：15 0 15 10 * * ? 2005 2005年的每天上午10：15 0 * 14 * * ? 每天下午14:00到14:59 0 0/5 14 * * ? 每天下午14:00到14:55之间，每5分钟一次 0 0/5 14,18 * * ? 每天下午14:00到14:55、18:00到18:55之间，每5分钟一次 0 0-5 14 * * ? 每天下午14:00到14:05之间，每分钟一次 0 10,44 14 ? 3 WED 三月份每个周三的14:10和14:44 0 15 10 ? * MON-FRI 周一到星期五的的上午10:15 0 15 10 15 * ? 每月15日的上午10:15 0 15 10 L * ? 每月最后一天的上午10:15 0 15 10 L-2 * ? 每月倒数第二天的上午10:15 0 15 10 ? * 6L 每月最后一个星期五的上午10:15 0 15 10 ? * 6L 2002-2005 2002到2005年，每个月的最后一个星期五的上午10:15 0 15 10 ? * 6#3 每个月的第三个星期五的15:10 0 0 12 1/5 * ? 每月1日开始，每隔五天的中午12:00:00 0 11 11 11 11 ? 每年11月11日11:11 ","date":"2017-09-14T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/cron-expression/","title":"Quartz Tutorial Cron Expression"},{"content":"Quartz Tutorial 5 - Cron Trigger 当你要求作业的调度方式是“日历式”而不是“给定时间间隔式”的时候，CronTrigger比SimpleTrigger好用多了。\n用CronTrigger的话，很容易就能声明类似“每周五下午”或者“每周工作日上午9:30”，甚至“一月份的每个周一周三周五的上午九点到十点之间，每五分钟”这种奇奇怪怪的触发时间。\n此外，CronTrigger也有个startTime属性用于声明何时强制调度，也有一个（可选的）endTime属性用于声明何时结束调度。\nCron Expressions Cron表达式是用于配置CronTrigger实例的。Cron表达式是由六个子表达式组成的字符串，它们互相独立地描述调度的各个细节。这些子表达式是由空格分割，它们表示：\n秒(Seconds) 分钟(Minutes) 小时(Hours) 月份中的天(Day-of-Month) 月份(Month) 一周中的天(Day-of-Week) 年（可选）(Year) 举个例子，假定有字符串\u0026quot;0 0 12 ? * WED\u0026quot;，它表示“每周三的中午12点”\n这些互相独立的子表达式可以包含范围和/或列表。举个例子，上面例子中的“一周中的天”字段可以替换成MON-FRI，MON,WED,FRI甚至可以是MON-WED,SAT\nCron表达式支持使用通配符。上例中，“月份中的天”表达式处的?表示每个“可能”的值，换言之，这个值是不确定的（但不是全部）。而对于*，就表示了所有值都是。\n每个子表达式都有其自己的定义域。这也很显而易见了-对于秒和分钟，肯定是0到59；对于小时也必然是0到23；对于月份中的天，定义域是1到31，但是你自己应当注意这个月实际的天数；月份可以是数字0到11，也可以是字符串“JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC”；一周中的天可以是数字1到7(1=Sunday)，也可以是字符串“SUN MON TUE WED THU FRI SAT”\n/ 用于声明值的增量。例如，分钟子表达式是0/15，它的意思是“在小时内，0分钟之后的每个第15分钟触发一次”；如果是3/20，意味着“小时内，3分钟之后的每个第20分钟触发一次”，换一个写法，分钟处的表达式就是3 23 43。要注意到一个细微的区别：/35不是说“每个35分”，而是说“从0分钟开始后的每个第35分钟”，换个写法就是0 35。\n? 可以用在月份中的天和一周中的天子表达式中。这个符号表示“非确定的值”。当你需要明确声明其中一个值，但是另一个无法明确指定的时候，这很有用。比如你想在每个星期四触发作业的执行，但是周四具体是几号其实是不确定的，那么此时在月份中的天子表达式处就可以写这个通配符了。\nL 可以用在月份中的天和一周中的天子表达式处。这是”last”的简写，但它在两个子表达式中具有不同的意义。例如，在月份中的天子表达式处填写L，意味着“月份中的最后一天”，例如一月份的31号，平年2月的28号等等；如果单独的L放在一周中的天子表达式处，意味着7或者SAT（周六）；但是在一周中的天子表达式中，如果L放在了其他值的后面，例如6L或FRIL，那就意味着“该月的最后一个周五”。你也可以声明相对于每月最后一天的偏移，例如L-3意味着该月的倒数第三天。当你使用L时，不要再声明列表式、范围式的值了，不然你将得到奇怪的结果\nW 用于声明距离给定日子最近的工作日（周一至周五）。例如你在月份中的天子表达式中声明了15W，那意味着该月“距离15号最近的工作日”\n# 用于声明“月份中第X个周X”。例如在一周中的天子表达式处给出6#3或者FRI#3，意思是“该月第三个周五”\n下面是一些Cron表达式及其含义的具体举例，当然你可以在org.quartz.CronExpression的JavaDoc中找到更多。\nExample Cron Expressions 每五分钟触发一次\n1 0 0/5 * * * ? 每五分钟的第十秒触发，例如“10:00:10, 10:05:10”\n1 10 0/5 * * * ? 每周三、周五的10:30, 11:30, 12:30, 13:30触发\n1 0 30 10-13 * * WED,FRI 每月5号和20号的上午8点到10之间，每半小时触发一次。要注意10点并不会触发，只是八点、八点半、九点、九点半\n1 0 0/30 8-9 5,20 * ? 有时有些调度的要求特别复杂，如果放在一个表达式中表示会过于复杂，例如“上午九点到十点之间每五分钟，下午一点到十点之间每二十分钟”。这种情况的话，可以创建两个触发器，然后将他俩都关联到同一个作业就好。\nBuilding CronTriggers CronTrigger的实例构造是通过TriggerBuilder类（设置触发器的主要共有属性）和CronScheduleBuilder类（设置CronTrigger类型触发器特有的属性）。首先静态导入如下内容，就能用DSL风格构造触发器实例了。\n1 2 3 import static org.quartz.TriggerBuilder.*; import static org.quartz.CronScheduleBuilder.*; import static org.quartz.DateBuilder.*; 每天上午的八点到下午五点，每两分钟触发一次\n1 2 3 4 5 trigger = newTrigger() .withIdentity(\u0026#34;trigger3\u0026#34;, \u0026#34;group1\u0026#34;) .withSchedule(cronSchedule(\u0026#34;0 0/2 8-17 * * ?\u0026#34;)) .forJob(\u0026#34;myJob\u0026#34;, \u0026#34;group1\u0026#34;) .build(); 每天上午10点42分触发\n1 2 3 4 5 6 7 8 9 10 11 trigger = newTrigger() .withIdentity(\u0026#34;trigger3\u0026#34;, \u0026#34;group1\u0026#34;) .withSchedule(dailyAtHouAndMinute(10, 42)) .forJob(myJobKey) .build() // or trigger = newTrigger() .withIdentity(\u0026#34;trigger3\u0026#34;, \u0026#34;group1\u0026#34;) .withSchedule(cronSchedule(\u0026#34;0 42 10 * * ?\u0026#34;)) .forJob(myJobKey) .build(); 在周三上午10点42触发，此外，时区与系统默认时区不同\n1 2 3 4 5 6 7 8 9 10 11 12 13 trigger = newTrigger() .withIdentity(\u0026#34;trigger3\u0026#34;, \u0026#34;group1\u0026#34;) .withSchedule(weeklyOnDayAdnHourAdnMinute(DateBuilder.WEDNESDAY, 10, 42)) .forJob(myJobKey) .inTimeZone(TimeZone.getTimeZone(\u0026#34;America/Los_Angeles\u0026#34;)) .build(); // or trigger = newTrigger() .withIdentity(\u0026#34;trigger3\u0026#34;, \u0026#34;group1\u0026#34;) .withSchedule(cronSchedule(\u0026#34;0 42 10 * * WED\u0026#34;)) .inTimeZone(TimeZone.getTimeZone(\u0026#34;America/Los_Angeles\u0026#34;)) .forJob(myJobKey) .build(); CronTrigger Misfire Instruction 当发生了misfire（“哑火”？），CronTrigger提供了一些指令用于告知Quartz该如何处理。这些指令是作为CronTrigger自身的常量来定义的（JavaDoc介绍了它们的行为），这些常量包括：\n1 2 3 MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY MISFIRE_INSTRUCTION_DO_NOTHING MISFIRE_INSTRUCTION_FIRE_NOW 所有类型的触发器都有Trigger.MISFIRE_INSTRUCTION_SMART_POLICY指令可用，这也是所有触发器默认的选择。在CronTrigger中，该“smart policy”会使用MISFIRE_INSTRUCTION_FIRE_NOW。CronTrigger.updateAfterMisfire()的JavaDoc介绍了具体的行为细节。\n当构造CronTrigger的实例时，你也可以声明该属性（通过CronScheduleBuilder）\n1 2 3 4 5 6 trigger = newTrigger() .withIdentity(\u0026#34;trigger3\u0026#34;, \u0026#34;group1\u0026#34;) .withSchedule(cronSchedule(\u0026#34;0 0/2 8-17 * * ?\u0026#34;) .withMisfireHandlingInstructionFireAndProceed()) .forJob(\u0026#34;myJob\u0026#34;, \u0026#34;group1\u0026#34;) .build() ","date":"2017-09-13T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/5.cron-trigger/","title":"Quartz Tutorial 5 - Cron Trigger"},{"content":"Quartz Tutorial 6 - TriggerListeners and JobListeners 监听器是在调度器内部基于事件执行动作的对象。顾名思义，TriggerListeners监听与触发器相关的事件，JobListeners监听与作业有关的事件。\n触发器相关的时间包括：触发、未触发（misfire）、触发结束（触发的作业执行完成）\norg.quartz.TriggerListener\n1 2 3 4 5 6 7 public interface TriggerListener { public String getName(); public void triggerFired(Trigger trigger, JobExecutionContext context); public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context); public void triggerMisfired(Trigger trigger); public void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode); } 作业相关的事件包括：作业将要执行的通知，作业完成的通知\norg.quartz.JobListener 1 2 3 4 5 6 public interface JobListener { public String getName(); public void jobToBeExecuted(JobExecutionContext context); public void jobExecutionVetoed(JobExecutionContext context); public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException); } Using Your Own Listeners 要创建一个监听器，需要创建一个实现了org.quartz.TriggerListener或者org.quartz.JobListener接口的类对象。然后在运行时向调度器注册，而且必须要给它一个名字（或者必须能通过getName()方法得知它们的名字）\n为了用着方便，除了动手实现接口之外，你也可以通过扩展JobListenerSupport或TriggerListenerSupport类，然后只需要重写你用得上的事件处理方法即可。\n监听器使用调度器的ListenerManager方法向调度器注册，此外还用到了一个匹配器用于描述该监听器需要监听的作业或触发器。\n监听器是运行时向调度器注册的，而且不随着作业或触发器一起存储在JobStore中。这是因为监听器一般作为与应用的集成点。因此，每次程序运行，监听器都要向调度器注册\n添加对特定作业监听的JobListener\n1 scheduler.getListenerManager().addJobListener(myJobListener, KeyMatcher.keyEquals(new JobKey(\u0026#34;myJobName\u0026#34;, \u0026#34;myJobGroup\u0026#34;))); 加入关于匹配器等其他类的静态导入，能把代码写得更清晰点：\n1 2 3 4 5 6 7 import static org.quartz.JobKey.*; import static org.quartz.KeyMatcher.*; import static org.quartz.GroupMatcher.*; import static org.quartz.AndMatcher.*; import static org.quartz.OrMatcher.*; import static org.quartz.EverythingMatcher.*; ... 上例就可以写成\n1 scheduler.getListenerManager().addJobListener(myJobListener, keyEquals(jobKey(\u0026#34;myJobName\u0026#34;, \u0026#34;myJobGroup\u0026#34;))); 添加对特定组内所有任务进行监听的监听器\n1 scheduler.getListenerManager().addJobListener(myJobListener, groupEquals(\u0026#34;myJobGroup\u0026#34;)); 添加对两个特定组内所有任务进行监听的监听器\n1 scheduler.getListenerManager().addJobListener(myJobListener, or(groupEquals(\u0026#34;myJobGroup1\u0026#34;), groupEquals(\u0026#34;myJobGroup2\u0026#34;))); 添加对所有任务进行监听的监听器\n1 scheduler.getListenerManager().addJobListener(myJobListener, allJobs()); 同样的，注册触发器监听器也是这样子。\n大部分Quartz的用户都不会用到监听器，但它还是很有用的，它可以让你的程序监听事件通知而作业本身又不需要显式通知。\n","date":"2017-09-13T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/6.triggerlistener-joblistener/","title":"Quartz Tutorial 6 - TriggerListeners and JobListeners"},{"content":"Quartz Tutorial 7 - SchedulerListeners SchedulerListener跟TriggerListener和JobListener很类似。当然，它接收来自调度器自身的事件通知，而不是某个特定的作业或者触发器。\n调度器相关的事件包括：作业/触发器的添加，作业/触发器的移除，一系列调度器内部的错误，调度器被关闭及其它事件。\norg.quartz.SchedulerListener\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface SchedulerListener { public void jobScheduled(Trigger trigger); public void jobUnscheduled(String triggerName, String triggerGroup); public void triggerFinalized(Trigger trigger); public void triggerPaused(String triggerName, String triggerGroup); public void triggerResumed(String triggerName, String triggerGroup); public void jobPaused(String jobName, String jobGroup); public void jobResumed(String jobName, String jobGroup); public void schedulerError(String msg, SchedulerException cause); public void schedulerStarted(); public void schedulerInStandbyMode(); public void schedulerShutdown(); public void schedulingDataCleared(); } 调度器监听器是使用调度器的ListenerManager来注册的。调度器监听器实际上可以是任何实现了org.quartz.SchedulerListener接口的对象。\n注册调度器监听器\n1 scheduler.getListenerManager().addSchedulerListener(mySchedulerListener); 移除调度器监听器\n1 scheduler.getListenerManager().removeSchedulerListener(mySchedulerListener); ","date":"2017-09-13T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/7.schedulerlistener/","title":"Quartz Tutorial 7 - SchedulerListeners"},{"content":"Quartz Tutorial 4 - Simple Trigger 当你的某个任务仅需要在某个特定时间执行一次，或者在某个特定时间之后以固定间隔重复特定的次数，此时就用到了SimpleTrigger了。例如，你需要在某年月日某时分秒触发作业的执行，或者在那个时间后再触发5次，间隔10秒钟。\n通过这些，很容易就能想到SimpleTrigger所包含的属性：开始时间，结束时间，重复次数，还有重复间隔。所有这些属性都确实存在，跟你想的确实一样。只是对于结束时间属性，有一些特殊的地方。\n重复的次数可以为0、正整数或者是一个常量SimpleTrigger.REPEAT_INDEFINITELY。重复间隔属性必须是0或者一个正的long类型数字，它表示毫秒数。要注意，当重复间隔设为0的时候，触发器将会被同步地触发（或者是调度器所能做到的尽可能靠近的触发）\n如果你熟悉Quartz的DateBuilder类，当你想通过你要创建的开始时间startTime(或结束时间endTime)来计算触发的次数，你就知道这个类特别有用。\n如果特别声明了结束时间endTime属性，，那么它将覆盖掉重复次数这一属性。比如你可以创建一个每十秒重复一次直到某个特定时间停止的触发器-你不需要计算在起止时间内到底重复了多少次，甚至你可以将重复次数属性赋值为REPEAT_INDEFINITELY（你也可以降之设定为一个足够大的值，保证该值大于它将要实际重复的次数）\nSimpleTrigger的实例构造是通过TriggerBuilder类（设置触发器的主要共有属性）和SimpleScheduleBuilder类（设置SimpleTrigger类型触发器特有的属性）。首先静态导入如下类型，就能用DSL风格构造触发器实例了。\n1 2 3 import static org.quartz.TriggerBuilder.*; import static org.quartz.SimpleScheduleBuilder.*; import static org.quartz.DateBuilder.*; 下面是定义触发器的多个例子，各自存在不同要点：\n特定时刻触发，无重复\n1 2 3 4 5 SimpleTrigger trigger = (SimpleTrigger) newTrigger() .withIdentity(\u0026#34;trigger1\u0026#34;, \u0026#34;group1\u0026#34;) .startAt(startTime) // some date .forJob(\u0026#34;job1\u0026#34;, \u0026#34;group1\u0026#34;) // identify job with name, group strings .build(); 特定时刻触发，每10秒触发一次，重复10次\n1 2 3 4 5 6 7 8 trigger = newTrigger() .withIdentity(\u0026#34;trigger3\u0026#34;,\u0026#34;group1\u0026#34;) .startAt(myTimeToStartFiring) // if a start time is not given(if this line we omitted), \u0026#34;now\u0026#34; is implied .withSchedule(simpleSchedule() .withIntervalInSeconds(10) .withRepeatCount(10)) // note that 10 repeats will given a total of 11 firings .forJob(myJob) // identify job with handle to its JobDetail itself .build(); 仅五分钟后触发一次\n1 2 3 4 5 trigger = (SimpleTrigger) newTrigger() .withIdentity(\u0026#34;trigger5\u0026#34;, \u0026#34;group1\u0026#34;) .startAt(futureDate(5, IntervalUnit.MINUTE)) // use DateBuilder to create a date in the future .forJob(myJob) // identify job with its JobKey .build(); 立即触发，然后每5分钟触发一次，直到22:00\n1 2 3 4 5 6 7 trigger = newTrigger() .withIdentity(\u0026#34;trigger7\u0026#34;, \u0026#34;group1\u0026#34;) .withSchedule(simpleSchedule() .withIntervalInMinutes(5) .reportForever()) .endAt(dateOf(22, 0, 0)) .build(); 下一小时一到立即触发，然后每两小时触发一次，永久重复\n1 2 3 4 5 6 7 8 9 10 11 trigger = newTrigger() .withIdentity(\u0026#34;trigger8\u0026#34;) // because group isn\u0026#39;t specificated, \u0026#34;trigger8\u0026#34; will be in the default group .startAt(evenHourDate(null)) //get the next even-hour (minutes and seconds zero (\u0026#34;00:00\u0026#34;)) .withSchedule(simpleSchedule() .withIntervalInHours(2) .repeatForever()) // note that in this example, `forJob(...)` is not called // - which is valid if the trigger is passed to the scheduler along with the job .build(); scheduler.scheduleJob(trigger, job); 花些时间看看在TriggerBuilder和SimpleScheduleBuilder中所有可用的方法，你能对于他们能做些什么更熟悉，也能了解到以上例子里没有展示到的特性\n要注意：TriggerBuilder（或者Quartz的其他builder）一般会选择一个合理的值赋给你没有显式赋值的属性。例如，你没有调用withIdentity(...)方法，那么TriggerBuilder将会生成一个随机的名字，如果你没有调用startAt(...)，它将会假定你要使用当前时间（立即触发）\nSimpleTrigger MisFire Instructions 当发生了misfire（“哑火”？），SimpleTrigger提供了一些指令用于告知Quartz该如何处理。这些指令是作为SimpleTrigger自身的常量来定义的（JavaDoc介绍了它们的行为），这些常量包括：\n1 2 3 4 5 6 7 8 // Misfire Instruction Constants of Simple Trigger MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY MISFIRE_INSTRUCTION_FIRE_NOW MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT 你可以回顾之前内容，所有类型的触发器都有Trigger.MISFIRE_INSTRUCTION_SMART_POLICY指令可用，这也是所有触发器默认的选择。\n如果使用了“smart policy”，SimpleTrigger在它多种选项内动态选择，具体取决于该触发器实例的配置和状态。SimpleTrigger.updateAfterMisfire()方法的JavaDoc解释了动态行为的更多细节。\n当然，你也可以在构建SimpleTrigger实例的时候设置上这一属性（通过SimpleScheduleBuilder）\n1 2 3 4 5 6 7 trigger = newTrigger() .withIdentity(\u0026#34;tigger7\u0026#34;, \u0026#34;group1\u0026#34;) .withSchedule(simpleSchedule() .withIntervalInMinutes(5) .repeatForever() .withMisfireHandlingInstructionNextWithExistingCount()) .build(); ","date":"2017-09-09T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/4.simple-trigger/","title":"Quartz Tutorial 4 - Simple Trigger"},{"content":"Quartz Tutorial 3 - More About Triggers 类似作业，触发器也是一样易于使用，只是它还包含了一些可自定义的选项。如果想要用Quartz用得飞起，那你需要了解它们。当然首先应该说明，Quartz提供了多种类型的触发器供你使用。\n在后面两节，你能了解两个主流使用的触发器SimpleTrigger和CronTrigger。\nCommon Trigger Attributes 先不提所有的触发器都会有的用于标识自己的TriggerKey属性，还有很多其它共有的属性，这些共有的属性可以在你构建一个触发器定义的时候使用TriggerBuilder进行设置。\n这些属性如下所示：\njobKey属性表示该触发器被触发时，应该执行的作业的标识符 startTime属性表示该触发器的调度器开始干活的时间。这个值是java.util.Data对象，定义了一个时刻。对于某些类型的触发器，它们确实是在开始时间触发的，而另一些就是简单标记了一个时间，其余的触发都是在这个时间之后的。比如你定义了一个触发器并告知调度器“一月份每隔五天”触发一次，然后你设置开始时间是四月一号，那么要过好几个月，该触发器才会被首次触发。 endTime属性表示该触发器的调度器停止干活的时间。 其他属性在接下来几个小节讨论\nPriority 有些时候会出现这样的情况，某个时刻有很多个触发器需要被触发，但是Quartz并没有足够的工作线程/资源让这些触发器通通立即触发。那么这个时候，你可能想要给他们指定一个优先级，来告知Quartz应该先触发哪个后触发哪个。这个时候就需要用到priority属性了。如果某个时刻有N个触发器需要被触发，但是只有Z个工作线程可用，那么前Z个优先级最高的触发器将会被首先触发。如果你没有显式指定优先级，那么触发器使用默认值5。任何整数值都可以用来指定优先级，包括负数。\n注意：优先级只是在多触发器在同一时刻触发的时候才会用于比较。一个应该在10:59的触发器毕竟早于11:00的触发器。\n注意：如果一个触发器的作业被检测到需要恢复，它的恢复调度与它原来的触发优先级相同\nMisfire Instructions 触发器的另一个重要属性是它的“哑火指令”。多种情况会导致一个触发错过了它的触发，比如调度器被关闭，比如Quartz的线程池没有可用线程去执行作业等。不同类型的触发器有不同的“哑火指令”。它们默认使用“智能策略”的指令，它对于不同类型的触发器及配置有着不同行为。当调度器启动，它搜索所有出现哑火的持续的触发器，然后按照它们各自的哑火指令更新它们的状态。你开始用Quartz，就应该去熟悉各个触发器的呀或执行，它们在JavaDoc中有着详细的解释。在之后对于某些触发器类型的讲解中，将会详细介绍各自的该属性。\nCalendars Quartz的Calendar对象(不是java.util.Calendar对象)能够在触发器被定义的时候与之关联并存到调度器中。当需要排除某些触发时间的时候，它很有用。举个例子，你可以在周工作日(周一到周五)上午九点半执行某个作业，然后可以添加一个Calendar排除那些法定假日。\nCalendar可以是任何实现了Calendar接口的可序列化对象。定义如下\n1 2 3 4 5 package org.quartz; public interface Calender { public boolean isTimeIncluded(long timeStamp); public long getNextIncludedTime(long timeStamp); } 注意这些方法的参数都是long类型的。你可能觉得有些不好，因为时间戳都精确到毫秒了，这是因为Quartz想要尽可能精确的去掉某些时间。但有些时候你可能需要去掉全天的，那么你可以使用org.quartz.impl.HolidayCalendar类，它就是做这个的。\nCalendar实例化后必须使用调度器的addCalendar()注册给调度器。如果你使用的是HolidayCalendar，实例化完成之后，你要使用addExcludedData(Date date)方法告知调度器你要排除的日子。同一个Calendar实例可以被多个触发器使用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 HolidayCalendar cal = new HolidayCalendar(); cal.addExcludedDate( someDate ); cal.addExcludedDate( someOtherDate ); sched.addCalendar(\u0026#34;myHolidays\u0026#34;, cal, false); Trigger t = newTrigger() .withIdentity(\u0026#34;myTrigger\u0026#34;) .forJob(\u0026#34;myJob\u0026#34;) .withSchedule(dailyAtHourAndMinute(9, 30)) // execute job daily at 9:30 .modifiedByCalendar(\u0026#34;myHolidays\u0026#34;) // but not on holidays .build(); // .. schedule job with trigger Trigger t2 = newTrigger() .withIdentity(\u0026#34;myTrigger2\u0026#34;) .forJob(\u0026#34;myJob2\u0026#34;) .withSchedule(dailyAtHourAndMinute(11, 30)) // execute job daily at 11:30 .modifiedByCalendar(\u0026#34;myHolidays\u0026#34;) // but not on holidays .build(); // .. schedule job with trigger2 触发器的实例化/构建的细节将会在后面机型讨论。上面的代码只是创建两个触发器，它们都是每日触发的，但都会跳过那些已排除的日期。\n如果有更多需要，请查看org.quartz.impl.calendar包，里面有Calendar的多种实现方式。\n","date":"2017-09-04T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/3.trigger/","title":"Quartz Tutorial 3 - More About Triggers"},{"content":"Quartz Tutorial 2 - More About Jobs and Job Details 就像是在上一节看到的，Job相当容易去实现，毕竟接口中只有一个execute方法。这里只需要再了解一些别的事情你就能理解作业的本质，关于Job接口中的execute方法，关于JobDetail类。\n当一个实现了Job接口的类定义完成，它要实际进行的工作也就很清晰了，但Quartz还需要知道你希望一个作业实例所具有的其它属性。那些属性是通过JobDetail类来告知Quartz的，前面已经简单介绍过。\nJobDetail实例是用JobBuilder类来构建的，可以使用静态导入该类中所有的静态方法，使代码具有DSL风格。\n1 import static org.quartz.JobBuilder.*; 首先花点时间讨论一下Job的本质和一个Job实例在Quartz内的生命周期。首先回顾一下第一节中的代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory(); Scheduler sched = schedFact.getScheduler(); sched.start(); // define the job and tie it to our HelloJob class JobDetail job = JobBuilder.newJob(HelloJob.class) .withIdentity(\u0026#34;myJob\u0026#34;, \u0026#34;group1\u0026#34;) .build(); // Trigger the job to run now, and then every 40 seconds Trigger trigger = TriggerBuilder .newTrigger() .withIdentity(\u0026#34;myTrigger\u0026#34;, \u0026#34;group1\u0026#34;) .startNow() .withSchedule(SimpleScheduleBuilder .simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build(); // Tell quartz to schedule the job using our trigger sched.scheduleJob(job, trigger); 作业的实体类HelloJob是这样定义的\n1 2 3 4 5 6 public class HelloJob implements Job { public HelloJob() {} public void execute(JobExecutionContext context) throws JobExecutionException { System.err.println(\u0026#34;Hello! HelloJob is executing.\u0026#34;); } } 注意到，我们给调度器传递了一个JobDetail实例，它知道我们要执行的作业的实际类型，因为我们构造它的时候传递进去了。每次调度器执行这个作业之前，它都会先创建一个新的HelloJob实例，然后调用execute方法。作业一旦执行完毕，调度器对这个对象实例的引用便会丢弃，这个实例就会被垃圾回收。能这样做的一个条件是，Job的实现类必须有一个公共的无参构造器(当然，此时使用的是默认的JobFactory)；另一个就是这个实现类不需要有什么表示状态的数据成员，这没什么意义，一旦作业执行完毕，这些数据成员也就不再保留了。\n那么我们如何为作业实例提供属性或者配置？或者怎样在执行过程中继续跟踪它的状态呢？这里提供了JobDataMap，它也是JobDetail对象的一部分。\nJobDataMap JobDataMap可以持有你想让作业执行期间能访问到的（可序列化的）数据对象，它是Map接口的一种实现，此外还添加了一些方法使之能方便存取基本类型的数据。\n如下一段代码就是在定义/构建JobDetail阶段，将一些数据放置到JobDataMap对象中。\n1 2 3 4 5 6 // define the job and tie it to our DumbJob class JobDetail job = newJob(DumbJob.class) .withIdentity(\u0026#34;myJob\u0026#34;, \u0026#34;group1\u0026#34;) // name \u0026#34;myJob\u0026#34;, group \u0026#34;group1\u0026#34; .usingJobData(\u0026#34;jobSays\u0026#34;, \u0026#34;Hello World!\u0026#34;) .usingJobData(\u0026#34;myFloatValue\u0026#34;, 3.141f) .build(); 如下代码就是在作业执行期间，去JobDataMap中获取数据\n1 2 3 4 5 6 7 8 9 10 11 public class DumbJob implements Job { public DumbJob() {} public void execute(JobExecutionContext context) throws JobExecutionException{ JobKey key = context.getJobDetail().getKey(); JobDataMap dataMap = context.getJobDetail().getJobDataMap(); String jobSays = dataMap.getString(\u0026#34;jobSays\u0026#34;); float myFloatValue = dataMap.getFloat(\u0026#34;myFloatValue\u0026#34;); System.err.println(\u0026#34;Instance \u0026#34; + key + \u0026#34; of DumbJob says: \u0026#34; + jobSays + \u0026#34;, and val is: \u0026#34; + myFloatValue); } } 如果你使用不变的JobStore（将会在本系列的JobStore一节讨论），你应当慎重决定将什么放入到JobDataMap，因为该对象将会被序列化，因此同意出现类版本问题。当然标准的Java类型应该很安全，但如果有人修改了类型定义而你又序列化了实例，要小心，免得破坏了兼容性。你也可以考虑将JDBC-JobStore和JobDataMap置于只能放基本类型和字符串的模式，这样就能消除未来可能的序列化问题。\n如果在你的作业类中添加了set访问器，而且它的名字与JobDataMap中某个键的名字是符合的（例如setJobSays(String val)与jobSays），那么Quartz的默认JobFactory类会在实例化这个作业的时候自动调用这些方法，这样就不用在代码中显式获取了。\n触发器也可以拥有与之关联的JobDataMap。当你有个储存在调度器中的作业要被多个触发器常规/重复使用的时候，它就非常有用了。对于每次互相独立的触发，你可以给作业提供不同的输入数据。\n在作业执行期间可以在JobExecutionContext找到一个JobDataMap，它合并了在JobDetail和Trigger中的JobDataMap，而且，对于相同名字的值，后者将会覆盖掉前者。\n下面是一个从JobExecutionContext中找到并使用合并后的JobDataMap的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 public class DumbJob implements Job { public DumbJob() {} public void execute(JobExecutionContext context) throws JobExecutionException { JobKey key = context.getJobDetail().getKey(); JobDataMap dataMap = context.getMergedJobDataMap(); // Note the difference from the previous example String jobSays = dataMap.getString(\u0026#34;jobSays\u0026#34;); float myFloatValue = dataMap.getFloat(\u0026#34;myFloatValue\u0026#34;); ArrayList state = (ArrayList)dataMap.get(\u0026#34;myStateData\u0026#34;); state.add(new Date()); System.err.println(\u0026#34;Instance \u0026#34; + key + \u0026#34; of DumbJob says: \u0026#34; + jobSays + \u0026#34;, and val is: \u0026#34; + myFloatValue); } } 或者也可以用到JobFactory的注入功能，如下例所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class DumbJob implements Job { String jobSays; float myFloatValue; ArrayList state; public DumbJob() {} public void execute(JobExecutionContext context) throws JobExecutionException { JobKey key = context.getJobDetail().getKey(); JobDataMap dataMap = context.getMergedJobDataMap(); // Note the difference from the previous example state.add(new Date()); System.err.println(\u0026#34;Instance \u0026#34; + key + \u0026#34; of DumbJob says: \u0026#34; + jobSays + \u0026#34;, and val is: \u0026#34; + myFloatValue); } public void setJobSays(String jobSays) { this.jobSays = jobSays; } public void setMyFloatValue(float myFloatValue) { myFloatValue = myFloatValue; } public void setState(ArrayList state) { state = state; } } 这段代码确实更长了，但是execute()方法也确实清晰了。而且像是set构造器可以用IDE自动生成。\nJob “Instance” 有很多用户对于一个“作业实例”的确切形式感到疑惑，我们在这一节和后面两小节将之解释清楚。\n你可以创建一个单独的作业类，然后通过在调度器内创建多个JobDetail实例来保存它的“实例定义”，每个JobDetail都有他自己的属性集合及JobDataMap，然后将它们都放到调度器中。\n例如，你可以创建一个实现了Job接口的类，叫做SalesReportJob。这个作业类需要接受一个参数，来得知消费者的名字然后生成针对他的消费报告。因此可能需要创建多个作业的定义，例如“SalesReportForJoe”或“SalesReportForMike”。\n当一个触发器被触发，其关联的JobDetail实例将会被加载，然后它用到的作业类将会通过调度器配置的JobFactory来实例化。默认的JobFactory只是简单的调用newInstance()，然后尝试调用名字和JobDataMap中名字匹配的set访问器来赋值。或许你会需要构造你自己的JobFactory的实现来做一些事，例如你自己的控制反转或依赖注入容器来生成/初始化作业实例。\n我们将每个JobDetail称为一个“作业定义”或者“作业明细的实例”；我们还将每个正在执行的作业称为一个“作业实例”或“作业定义的一个实例”。通常，如果我们只是简单用到了“作业”一词，这指的是一个命名好的定义，或者作业明细。如果我们要说Job接口的实现类，我们会使用“作业类”这个词语。\nJob State and Concurrency 现在我们将解释一些关于作业的状态数据(JobDataMap)和并发性。它们是能加到你的作业类定义上的注解，它们将会影响Quartz的行为。\n@DisallowConcurrentExecution注解是加到作业类上的，它告知Quartz不要并发执行给定作业的多个实例。注意这里的措辞，这是反复推敲过的。在上一节的例子中，如果“SalesReportJob”有这个注解，在给定时刻，只能有一个“SalesReportForJoe”运行，但是它可以和一个“SalesReportForMike”同时运行。这个限制是基于JobDetail，而不是基于作业类的实例。\n@PersistJobDataAfterExecution也是给作业类加的注解。它告知Quartz，只有execute方法完全成功执行完毕之后才更新存储JobDetail的JobDataMap，因为同一个作业的下一次执行将会使用更新后的值，而不是原来的值。就像上面介绍的@DisallowConcurrentExecution注解，它也是限制作业定义实例而不是作业类实例的。\n如果你使用了@PersistJobDataAfterExecution注解，那你也应当认真考虑是否也使用@DisallowConcurrentExecution注解，这是为了避免同一作业的两个实例同时执行时出现可能的竞态条件。\nOther Attributes Of Jobs 这里是其它能通过JobDetail对象对作业进行定义的属性：\nDurability - 如果一个作业是非耐用的，那么一旦调度器中没有活动触发器与之关联，他将会被自动删除。换句话说，非持久作业的生命周期受到与它关联的触发器限制。 RequestsRecovery - 如果一个作业“请求恢复”，而且在它执行期间被调度器“强制关闭”（例如进程运行中崩溃，或者电脑关机），那么当调度器再次启动时，这个作业也将再次执行。这种情况下，JobExecutionContext.isRecovering()将返回true。 JobExecutionException 最后，我们需要介绍Job.execute方法的一些其它细节。在该方法中，唯一能丢出的异常（包括运行时异常）类型是JobExecutionException。因此你应该使用try-catch语句块包裹execute方法的全部内容。你也应当花时间看看这个异常的文档，因为你的作业能够适应该异常来告知调度器具体使用哪种方式来处理这个出现异常的作业。\n","date":"2017-09-03T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/2.job-and-job-detail/","title":"Quartz Tutorial 2 - More About Jobs and Job Details"},{"content":"Quartz Tutorial 1 - Using Quartz \u0026amp; The Quartz API, Jobs and Trigger 建议从Getting started with Quartz开始，先写一个非常简单的demo，基于这个demo，去理解后面的内容。\nUsing Quartz 在使用调度器之前，应当先实例化一个，这便用到了SchedulerFactory。也有人将factory的一个实例保存在了JDNI(Java Naming and Directory Interface)存储，因此实例化一个调度器也就更容易了。\n调度器一旦被实例化完成，它可以被启动，可以处于等待模式，也可以被关闭。注意，一旦一个调度器关闭了，如果不重新实例化，它就不能重新启动。如果调度器并未被启动，或者整出一暂停状态，触发器也不会被触发（作业并不会执行）。\n以下代码片段实例化并启动了一个调度器，并调度执行了一个作业\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory(); Scheduler sched = schedFact.getScheduler(); sched.start(); // define the job and tie it to our HelloJob class JobDetail job = JobBuilder.newJob(HelloJob.class) .withIdentity(\u0026#34;myJob\u0026#34;, \u0026#34;group1\u0026#34;) .build(); // Trigger the job to run now, and then every 40 seconds Trigger trigger = TriggerBuilder .newTrigger() .withIdentity(\u0026#34;myTrigger\u0026#34;, \u0026#34;group1\u0026#34;) .startNow() .withSchedule(SimpleScheduleBuilder .simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build(); // Tell quartz to schedule the job using our trigger sched.scheduleJob(job, trigger); 如上所示，用Quartz干活就是这么简单。\nThe Quartz API, Jobs and Trigger Quratz API 的核心接口如下\nScheduler - 和调度器进行交互的核心API Job - 你需要去实现的接口，里面放着你想让调度器执行的 JobDetail - 用于定义Job的实例 Trigger - 一个用于告知调度器去执行哪个作业的组件 JobBuilder - 用于定义/构建JobDetail的实例 TriggerBuilder - 用于定义/构建Trigger的实例 一个调度器(Scheduler)的生命周期以SchedulerFactory对它的创建为起始，到调用它的shutdown()为终止。创建了调度器实例后，就能添加、移除或者列出触发器及作业，或者执行其它调度相关的操作（例如暂停一个触发器）。但是，在调度器启动起来之前，它不会对任何触发器或作业执行任何实际操作。\nQuartz提供了一系列定义了领域专用语言(DSL)的builder类，上一节已经见到过了，此处我们再贴出来\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // define the job and tie it to our HelloJob class JobDetail job = newJob(HelloJob.class) .withIdentity(\u0026#34;myJob\u0026#34;, \u0026#34;group1\u0026#34;) // name \u0026#34;myJob\u0026#34;, group \u0026#34;group1\u0026#34; .build(); // Trigger the job to run now, and then every 40 seconds Trigger trigger = newTrigger() .withIdentity(\u0026#34;myTrigger\u0026#34;, \u0026#34;group1\u0026#34;) .startNow() .withSchedule(simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build(); // Tell quartz to schedule the job using our trigger sched.scheduleJob(job, trigger); 构建一个Job使用的方法是我们从JobBuilder类中静态导入的；同样的，构建一个触发器的方法也是从TriggerBuilder类中静态导入的，也有来自SimpleScheduleBuilder类中的。\nDSL的静态导入可以通过以下这些import语句做到：\n1 2 3 4 5 6 import static org.quartz.JobBuilder.*; import static org.quartz.SimpleScheduleBuilder.*; import static org.quartz.CronScheduleBuilder.*; import static org.quartz.CalendarIntervalScheduleBuilder.*; import static org.quartz.TriggerBuilder.*; import static org.quartz.DateBuilder.*; 这些ScheduleBuilder类有创建不同类型调度器的方法。其中DateBuilder类包含的一系列方法，能很容易创建各种“奇怪”时间点的java.util.Date的实例。\nJobs and Triggers 一个作业(Job)指的是一个实现了Job接口的类，该接口只有一个简单的方法\n1 2 3 4 5 6 package org.quartz; // the Job interface public interface Job { public void execute(JobExecutionContext context) throws JobExecutionException; } 当该作业的触发器被触发，它的execute方法就会被调度器的一个工作线程激活。传给这个方法的JobExecutionContext对象给这个作业对象传递了它“运行时刻”的环境信息-调度执行它的调度器的句柄，被触发从而引发该方法执行的那个个触发器的句柄，这个作业的JobDetail对象，还有一些其它的东西。\n当作业被加入到调度器的时候，Quartz客户端(就是你的程序)就创建了一个JobDetail对象。它包含了各种用于设置这个作业的属性，包括一个JobDataMap，它可用于保存给定的作业类实例的状态信息。它是作业实例的本质定义，我们将会在下一节讨论它。\nTrigger对象用于触发作业的执行。当你希望调度一个作业时，实例化一个触发器然后调整它的属性，使之调度安排符合预期。触发器也有与之关联的JobDataMap-当需要给作业传递特定参数的时候，它就非常有用了。Quartz提供了多种类型的触发器，但大家大多使用SimpleTrigger和CronTrigger。\n当你需要“单触发”的运行（就是在某个给定时刻，某个作业只有单个的执行），或者你需要在给定的时刻触发作业，重复多次，在多次执行之间需要延迟，SimpleTrigger是非常好用的。如果你需要“类日历式”调度作业，例如“每周五下午”或者“每月10日的上午10:15”，CronTrigger是非常好用的。\n为何Job和Trigger要组合存在呢？很多调度器并不刻意区分作业和触发器的概念。有些定义一个作业只是简单定义了一个执行时间（或调度）和一些小的作业标识符，有的就比较像是组合了Quartz的作业和触发器对象。开发Quartz的时候，我们决定分离调度和调度要执行的作业，是因为（我们认为）这有很多益处。\n例如，作业可以被创建然后存储在调度器中，它跟触发器互相独立，多个触发器可以关联一个作业。另一个解耦的优势在于，当与某个作业关联的触发器被销毁之后，调度器依然能持有这个作业，使之能被重新调度，而不需要在定义一个该作业。这种做法也允许你修改或者替换触发器，而不用重新定义它关联的作业。\nIdentities 作业和触发器在被注册到Quartz调度器时，都给定了一个特征码。作业和调度器的特征码的存在，允许它们可以被放置在“group”中，这样就能更方便地管理作业和触发器了。在组内部，触发器或作业地名称必须是唯一的，作业或触发器的特征码是自身名和组名组合起来的。\n","date":"2017-08-28T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/library/quartz-tutorial/1.using-quartz/","title":"Quartz Tutorial 1 - Using Quartz \u0026 The Quartz API, Jobs and Trigger"},{"content":"Learn Windows PowerShell 3 in a Month of Lunches - Review lab 1 (chapters 1–6) NOTE: To complete this lab, you will need any computer running PowerShell v3. You should complete the labs in chapters 1 through 6 of this book prior to attempting this review lab.\nHINTS :\nSort-Object Select-Object Import-Module Export-CSV Help Get-ChildItem (Dir) TASK 1\nRun a command that will display the newest 100 entries from the Application event log. Do not use Get-WinEvent\n1 Get-EventLog -LogName Application -Newest 10 TASK 2\nWrite a command line that displays only the five top processes based on virtual memory (VM) usage.\n1 Get-Process | Sort-Object -Property VM -Descending | Select-Object -First 5 TASK 3\nCreate a CSV file that contains all services, including only the service names and statuses. Have running services listed before stopped services.\n1 Get-Service | Sort-Object -Property Status -Descending | Select-Object -Property Name,Status | Export-Csv -Path C:\\service.csv TASK 4\nWrite a command line that changes the startup type of the BITS service to Manual\n1 Set-Service -Name BITS -StartupType Manual TASK 5\nDisplay a list of all files named win. on your computer. Start in the C:\\folder.\nNote: you may need to experiment and use some new parameters of a cmdlet in order to complete this task.\n1 Get-ChildItem C:\\ -Filter win*.* -File -Recurse TASK 6\nGet a directory listing for C:\\Program Files. Include all subfolders, and have the directory listing go into a text file named C:\\Dir.txt (remember to use the \u0026gt; redirector, or the Out-File cmdlet).\n1 Get-ChildItem \u0026#39;C:\\Program Files\u0026#39; -Recurse -Directory \u0026gt; C:\\dir.txt TASK 7\nGet a list of the most recent 20 entries from the Security event log, and convert the information to XML . Do not create a file on disk: have the XML display in the console window.\nNote that the XML may display as a single top-level object, rather than as raw XML data—that’s fine. That’s just how PowerShell displays XML . You can pipe the XML object to Format-Custom to see it expanded out into an object hierarchy, if you like.\n1 Get-EventLog -LogName Security -Newest 20 | ConvertTo-Xml TASK 8\nGet a list of services, and export the data to a CSV file named C:\\services.csv.\n1 Get-Service | Export-Csv C:\\services.csv TASK 9\nGet a list of services. Keep only the services’ names, display names, and statuses, and send that information to an HTML file. Have the phrase “Installed Services” displayed in the HTML file before the table of service information.\n1 Get-Service | Select-Object -Property Name,Status,DisplayName | ConvertTo-Html -PreContent \u0026#34;Installed Services\u0026#34; | Out-File C:\\services.html TASK 10\nCreate a new alias, named D , which runs Get-ChildItem. Export just that alias to a file.\nNow, close the shell and open a new console window. Import that alias into the shell.\nMake sure you can run D and get a directory listing.\n1 2 3 4 New-Alias -Name D -Value Get-ChildItem Export-Alias -Path C:\\alias.xml -Name D # reopen ps Import-Alias -Path C:\\alias.xml TASK 11\nDisplay a list of event logs that are available on your system.\n1 Get-EventLog -List TASK 12\nRun a command that will display the current directory that the shell is in.\n1 2 3 4 5 pwd # or Get-Location # or gl TASK 13\nRun a command that will display the most recent commands that you have run in the shell. Locate the command that you ran for task 11. Using two commands connected by a pipeline, rerun the command from task 11.\nIn other words, if Get-Something is the command that retrieves historical commands, if 5 is the ID number of the command from task 11, and Do-Something is the command that runs historical commands, run this:\nGet-Something –id 5 | Do-Something\nOf course, those aren’t the correct cmdlet names—you’ll need to find those. Hint: both commands that you need have the same noun.\n1 Get-History -Id 6 | Invoke-History TASK 14\nRun a command that modifies the Security event log to overwrite old events as needed.\n1 Limit-EventLog -LogName Security -OverflowAction OverwriteAsNeeded TASK 15\nUse the New-Item cmdlet to make a new directory named C:\\Review. This is not the same as running Mkdir ; the New-Item cmdlet will need to know what kind of new item you want to create. Read the help for the cmdlet.\n1 New-Item -Path C:\\Review -ItemType Directory TASK 16\nDisplay the contents of this registry key: HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\nNote: “User Shell Folders” is not exactly like a directory. If you change into that “directory,” you won’t see anything in a directory listing. User Shell Folders is an item, and what it contains are item properties. There’s a cmdlet capable of displaying item properties (although cmdlets use singular nouns, not plural).\n1 Get-ItemProperty \u0026#39;HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\u0026#39; TASK 17\nFind (but please do not run) cmdlets that can…\nRestart a computer Shut down a computer Remove a computer from a workgroup or domain Restore a computer’s System Restore checkpoint 1 help *computer* TASK 18\nWhat command do you think could change a registry value? Hint: it’s the same noun as the cmdlet you found for task 16.\n1 Set-ItemProperty ","date":"2017-08-14T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/language/learn-powershell-3/","title":"Learn Windows PowerShell 3 in a Month of Lunches - Review lab 1 (chapters 1–6)"},{"content":"CSS 选择器 对CSS选择器的相关知识一直停留在有些印象的层面，到了用的时候还是要来金老师的网站翻PPT。虽然每次看一遍都觉得差不多了，但实际是代码写的太少，依旧是差得远了。对于html+css+js认知地水平还是很水的，也就是了解些基本的东西，任重道远。本文是对CSS选择器基本知识的复习（预习）。\n基本选择器 标记选择器\n设置HTML中某标签的样式\n1 2 3 4 5 6 7 8 9 10 11 12 13 [label]{ /*style content*/ } /* for example */ p { color: red; font-size: 25px } /* use it \u0026lt;p\u0026gt;This is a \u0026#34;p\u0026#34; label.\u0026lt;/p\u0026gt; */ 类别选择器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 .[class-name] { /*style content*/ } /* for example */ .red_large_text { color: red; font-size: 30px } /* \u0026lt;p class=\u0026#34;red_large_text\u0026#34;\u0026gt;this is a paragraph.\u0026lt;/p\u0026gt; \u0026lt;a class=\u0026#34;red_large_text\u0026#34; href=\u0026#34;http://www.baidu.com\u0026#34;\u0026gt;Baidu\u0026lt;/a\u0026gt; */ ID选择器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #[id-name] { /* style content */ } /* for example */ #copyright { font-style:italic; font-size:16px; } /* \u0026lt;div id=\u0026#34;copyright\u0026#34;\u0026gt;Copyright ..... \u0026lt;/div\u0026gt; */ 通用选择器\n*是一个通配符，它匹配任何元素\n1 2 3 * { /* style content */ } 复合选择器 将以上基本选择器组合应用\n交集选择器\n直接指定特定标记中，特定样式类或id的元素样式\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [label-name][.[class-name]|#[id-name]] { /* style content */ /* 选择器之间不能有字符 */ } /* for example */ p.special{ /* 标记.类别选择器 */ color:red; } p#special{ /* id选择器 */ color:green; } /* \u0026lt;p\u0026gt; 本段未指定任何样式，以浏览器默认字体显示 \u0026lt;/p\u0026gt; \u0026lt;p class=\u0026#34;special\u0026#34;\u0026gt; 本段指定了.special类别的样式，适用于交集选择器“p.special”，字体为红色 \u0026lt;/p\u0026gt; \u0026lt;p id=\u0026#34;special\u0026#34;\u0026gt; 本段的id=special，适用于交集选择器“p#special”，字体为绿色 \u0026lt;/p\u0026gt; */ 并集选择器\n一次定义多个标签或类别或ID的样式\n1 2 3 4 5 6 7 8 9 10 11 [label],[.[class-name]],[#[id-name]] { /* style content */ /* 以逗号隔开各个选择器 */ } /* for example */ div,.special,#one { text-decoration:underline; } /* 凡是名字是\u0026lt;div\u0026gt;，其样式类名为special、id值为one的元素，其文本全都加上下划线 */ 后代选择器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [label1] [label2] { /* style content */ /* 以空格隔开各个选择器 */ /* 设定存在于元素label1中元素label2的样式 */ } [label1]\u0026gt;[label2] { /* style content */ /* 设定存在于元素label1中元素label2的样式，而且必须是label1的直接子元素 */ } /* for example */ p span{ /*凡是\u0026lt;p\u0026gt;元素中的\u0026lt;span\u0026gt;元素，全部以红色显示。*/ color:red; } div\u0026gt;h2 { /*只选择 h2 元素，并且这些元素都是 div 的 直接 子元素*/ color:red; } 兄弟选择器\n1 2 3 4 5 6 7 8 9 10 [label1]+[label2] { /* style content */ /* 选择label2元素，此元素是label1元素的弟弟（出现在label1下方），且是紧挨着的 */ } [label1]~[label2] { /* style content */ /* 选出所有label1的“弟弟”，不管是不是紧挨着的。 */ } /* for example */ 属性选择器 选择器 说明 a[href] 选择所有拥有指定href属性的a元素 a[href=\u0026quot;home.htm\u0026quot;] 仅选择具有特定属性值的a元素 img[alt~=\u0026quot;thumbnail\u0026quot;] 选择的img元素的alt属性中包容thumbnail单词（前后有空格） a[href^=\u0026quot;http://\u0026quot;] 选择所有href属性以“http://”开头的a元素 a[href$=\u0026quot;.pdf\u0026quot;] 选择所有href属性以“.pdf”结尾的a元素 div[id*=\u0026quot;main\u0026quot;] 选择所有id属性值包容“main”的所有div元素 伪类选择器 \u0026lt;a\u0026gt;的伪类\n超链接标签\u0026lt;a\u0026gt;支持几个特殊的CSS样式类，用于定义超链接不同状态的样式，这些样式被称为伪类（pseudo class）\n属性 说明 a:link 超链接的普通样式，即正常浏览状态的样式 a:visited 被点击之后的样式 a:hover 鼠标指针悬停在超链接元素之上时呈现的样式 a:active 单击超链接时呈现的样式 通用的伪类选择器\n属性 说明 :enabled 元素激活时 :disabled 元素被屏蔽时 :checked 元素处于选中状态 :focus 元素拥有焦点时 伪类选择器的“条件运算”\n伪类选择器支持not\ndiv:not(.myclass): 选择所有div元素，且其class属性不是myclass\n可以连续使用多个not\ndiv:not(.myclass1):not(.myclass2): 选择所有div元素，其class属性不是.myclass1和.myclass2\n可以使用其他条件\ndiv:not([id^=“main”]): 选择所有div元素，其id属性不是以main打头的\ndiv:target\n结构化选择器\n与DOM密切相关\n伪类选择器 说明 :root 选择根元素\u0026lt;html\u0026gt; :empty 选择空元素，例如 \u0026lt;p\u0026gt;\u0026lt;/p\u0026gt; :first-child 选择的元素是其父元素的第一个子元素 :last-child 选择的元素是其父元素的最后一个子元素 :first-of-type 指定元素类型的第一个儿子 :last-of-type 指定元素类型的最后一个儿子 :only-child 选中的元素是父元素的唯一儿子 :only-of-type 在父元素的所有儿子中，选择那些只有一个元素的元素类型 Nth类型选择器\n选择器 说明 :nth-child(n) 第几个孩子 :nth-last-child(n) 倒数第几个孩子 :nth-of-type(n) 第几个元素类型 :nth-last-of-type(n) 倒数第几个元素类型 Nth类型支持简单的计算表达式\n:nth-child(even) :nth-child(odd) :nth-child(an+b) a表示“步长”，为正表示向后，为负表示向前，b表示从第几个元素开始 伪元素 伪元素就是文档中若有实无的元素\n::first-letter（首字母）和 ::first-line（首行）\n1 2 3 4 p::first-letter { /*段落首字符放大3倍*/ font-size:300%; } ::before 和 ::after\n1 2 3 4 5 6 7 8 9 10 11 12 13 p.age::before { content:\u0026#34;Age: \u0026#34;; } p.age::after { content:\u0026#34; years.\u0026#34;; } /* \u0026lt;p class=\u0026#34;age\u0026#34;\u0026gt;25\u0026lt;/p\u0026gt; 显示效果： Age: 25 years. */ 继承与层叠原则 没有定义CSS规则的HTML元素从它的父元素中继承样式。\n如果相互冲突： 行内元素 \u0026gt; ID样式 \u0026gt; 类别样式 \u0026gt; 标签样式\n","date":"2017-07-29T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/language/css-selector/","title":"CSS选择器"},{"content":"F#-Notes-1 Functional Programming 在目前工作中，和看得见的之后吧。于我而言F#可能终归是一门用的少的语言，只能拿来当作兴趣的那种，而且身边也确实还没碰见对这门语言有兴趣的人，对它的学习和使用也总是会被打断。N久之后，虽然还记得一些思路上的东西，或者语言特性上的可能性，但是语法上的写法反而就这么忘记了，因此在此备忘一下。\n此外还有十分重要的一点原因，实在是因为F# for fun and profit的离线版读到一半之后实在读不下去了（大约2000页，读到1200页左右），在Understanding Parser Combinators一章最终决定放弃这本书。有一说一，这本书确实很好，也很适合有C#、Java、Python基础的去读。但是对我而言，战线拉得过长了。在这一章节，回想以前的内容，只能有一些大致的印象了，而不是特别有把握地回想起某某某怎样怎样。\n也可能是过于急于求成了，想要马上看到效果，欲速则不达。我这本F#的入门书其实看了可能有将近一年了吧？什么时候开始的已经忘记了，可能真的一年多了，到现在忘记了，很正常，毕竟平时并没有在使用。但是再次打开那个将近2000页的PDF也没有时间和勇气了，因此要按照“常规”的方式进行好了。\n其实坦白来讲，似乎只需要一个F# Cheatsheet就够了，但是担心还是会有什么漏掉的，毕竟自己的F#目前就是个半瓶子水。还是重新来一遍吧。\n内容参照《Beginning F# 4.0》进行整理，但是大致看一下这本书的目录，讲道理似乎不怎么样……嗯，同很多谈F#的编程书差不多，过于强调面向对象及命令式编程，而有些忽视F#基因中的味道，甚至对于更高级的内容避而不谈，这对于“关注语言特性”的我来说简直是个噩耗。不过也没有办法，目前这个工作强度，也确实没有大块时间去认真品味它，因此只能从皮毛入手了。算了不要在意这些细节，随缘好了。\n也会迟疑，连个let a = 0这种水平的东西也要记下来，是不是真的有必要？ 毕竟这东西也太小白了不是。似乎我们对于自己已经会了的东西总是觉得很傻逼，可能是“会者不难、难者不会”，等到忘了还是得回来找，因此还是记下来吧，容易的简单写写就好了。\nLiterals 数值这边跟.NET基类库大致一一对应，包括字符串、字符、布尔、有/无符号（长/短）整型，单/双精度浮点型等等。此外F#还自行扩展出了Microsoft.FSharp.Math.BigInt Microsoft.FSharp.Math.BigNum（在F#中用bigint bignum）\nAnonymous Functions 匿名函数（相对于具名函数而言），就是木有名字的函数了，也叫 lambda function(λ function)，或简称为lambda。\n1 fun x y -\u0026gt; x + y Identifiers and let/use Bindings 面对let a = 10这种表达式，很明显，这就是声明变量并初始化嘛。并不是，这是将值绑定到一个标识符，好吧场面上没什么区别的样子？要注意，a一旦被绑定，就不可变(因为它没有使用mutable显式声明可变)。此外在F#中，所有都是“值(Value)”，数值或者函数值，因此也可以将一个函数绑定到标识符上。\n关于绑定函数，可以选择绑定lambda到一个标识符的写法，也可以选择“直接声明函数”式的写法。\n一般绑定使用了let关键字，但是也有一个use关键字，它类似C#中的using，用于绑定需要尽快释放的资源。一旦脱离了use绑定的标识符的作用域，资源将会被自动释放。\nF#中作用域(Scope)是通过缩进控制的，这很类似Python了，不过一般不太需要游标卡尺（吧……）。此外一些基本的东西在此处就可用了，比如内部的作用域可以捕获外部作用域的标识符等等，这很直观了。\n1 2 3 4 5 6 7 8 9 10 11 let a = 10 // variable let add = fun x y -\u0026gt; x + y // anonymous function let raisePowerTwo x = x ** 2.0 // function open System.IO // function to read first line from a file let readFirstLine filename = // open file using a \u0026#34;use\u0026#34; binding use file = File.OpenText filename file.ReadLine() // call function and print the result printfn \u0026#34;First line was: %s\u0026#34; (readFirstLine \u0026#34;mytext.txt\u0026#34;) Recursion 递归的话，需要使用rec关键字来显示声明此函数是递归的。\n1 2 3 4 5 6 // assume x \u0026gt; 0 let rec fib x = match x with | 1 -\u0026gt; 1 | 2 -\u0026gt; 1 | x -\u0026gt; fib (x-1) + fib (x-2) Operators F#中操作符分“前缀操作符”和“中缀操作符”，好吧其实就是一元和二元了。操作符本质也是函数的体现，给操作符加上括号之后，就成为了一个函数，就能用应用函数的风格去使用。当然，也可以自定义操作符了，甚至重定义系统中存在的操作符。\n1 2 3 4 1 + 1 (+) 1 1 let add = (+) let ( +** ) x y = (x + y) * (x + y) Function Application and Partial Application 应用一个函数的时候，可以一次性给出全部参数，也可以先给出部分参数，最终求值的时候凑齐全部参数，非常灵活。（只针对F#中定义的函数，对于其它非F#语言写的库，如果原生方式使用，必须一次性给出全部参数，而且必须用括号包起来，类似元组。当然，可以用F#包装一下原生方法，使之成为F#的函数）\n1 2 3 4 5 6 let add x y = x + y let result0 = add 4 5 let result1 = add 1 2 | add 3 | add 4 let add1 = add 1 let result2 = add1 2 Pattern Matching 模式匹配是超级超级强大的，思想上转变比较难的话，可以认为这是个牛逼版的switch语句。\n关于它的花式运用直接看代码就好了，非常直观。\n此外关注function关键字。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 let rec luc x = match x with | x when x \u0026lt;= 0 -\u0026gt; failwith \u0026#34;value must be greater than 0\u0026#34; | 1 -\u0026gt; 1 | 2 -\u0026gt; 3 | x -\u0026gt; luc (x - 1) + luc (x - 2) let booleanToString x = match x with false -\u0026gt; \u0026#34;False\u0026#34; | _ -\u0026gt; \u0026#34;True\u0026#34; let stringToBoolean x = match x with | \u0026#34;True\u0026#34; | \u0026#34;true\u0026#34; -\u0026gt; true | \u0026#34;False\u0026#34; | \u0026#34;false\u0026#34; -\u0026gt; false | _ -\u0026gt; failwith \u0026#34;unexpected input\u0026#34; let myOr b1 b2 = match b1, b2 with | true, _ -\u0026gt; true | _, true -\u0026gt; true | _ -\u0026gt; false // function keyword let boolToInt = function true -\u0026gt; 1 | false -\u0026gt; 0 let rec concatStringList = function | head :: tail -\u0026gt; head + concatStringList tail | [] -\u0026gt; \u0026#34;\u0026#34; Control Flow 就是烂大街的if-else，一般来说更推荐上文模式匹配的写法。\n1 2 3 4 5 6 7 8 9 10 let result = if System.DateTime.Now.Second % 2 = 0 then \u0026#34;heads\u0026#34; else \u0026#34;tails\u0026#34; let result = match System.DateTime.Now.Second % 2 = 0 with | true -\u0026gt; \u0026#34;heads\u0026#34; | false -\u0026gt; \u0026#34;tails\u0026#34; Type F#是一门静态类型的强类型语言，它的的类型推断能力十分强大的，一般来说，如果不是为了特意限制，是不必加上类型限定符的。当然加上之后也无所谓了，下面的例子只是展示用法，并无太大实际意义，尤其是let绑定处的类型声明。\n1 2 3 let doNothingToAnInt (x: int) = x let intList = [1; 2; 3] let (stringList: list\u0026lt;string\u0026gt;) = [\u0026#34;one\u0026#34;; \u0026#34;two\u0026#34;; \u0026#34;three\u0026#34;] Defining Type 定义类型，在面向对象语言中似乎是语言学习的一个重点所在了，而在F#中，这只是非常小的一个方面了。而且下面要介绍的几个方面也是很小的一部分了。\nF#中，类型定义大致分三类，元组/记录类型，联合体类型，类类型（后面讨论，面向对象部分），这里只谈前两个。\nTuple and Record Types\n元组和记录型类型定义，没有太多不一样的地方了，看代码就好了\n1 2 3 4 5 6 7 type Name = string type Fullname = string * string type Organization = { chief: string; indians: string list } let wayneManor = { chief = \u0026#34;Batman\u0026#34;; indians = [\u0026#34;Robin\u0026#34;; \u0026#34;Alfred\u0026#34;] } Union Types\n联合类型，某种程度上来说，类似C中的union吧，这个类型在F#中非常常见，与模式匹配互相组合能发挥巨大的作用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Volume = | Liter of float | UsPint of float | ImperialPint of float let vol1 = Liter 2.5 let vol2 = UsPint 2.5 let vol3 = ImperialPint (2.5) type Shape = | Square of side:float | Rectangle of width:float * height:float | Circle of radius:float let S s = match s with | Square s -\u0026gt; s * 4.0 | Rectangle (w, h) -\u0026gt; (w + h) * 2.0 | Circle r -\u0026gt; System.Math.PI * r * 2.0 let sq = Square 1.2 let sq2 = Square(side=1.2) let rect3 = Rectangle(height=3.4, width=1.2) Generic (Type Definitions with Type Parameters)\n类型参数化，或者说，泛型，是一个很普遍的技术了，在保证类型安全的前提下提升了代码复用。语法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 type \u0026#39;a BinaryTree = | BinaryNode of \u0026#39;a BinaryTree * \u0026#39;a BinaryTree | BinaryValue of \u0026#39;a let tree1 = BinaryNode( BinaryNode ( BinaryValue 1, BinaryValue 2), BinaryNode ( BinaryValue 3, BinaryValue 4) ) type Tree\u0026lt;\u0026#39;a\u0026gt; = | Node of Tree\u0026lt;\u0026#39;a\u0026gt; list | Value of \u0026#39;a let tree2 = Node( [ Node( [Value \u0026#34;one\u0026#34;; Value \u0026#34;two\u0026#34;] ) ; Node( [Value \u0026#34;three\u0026#34;; Value \u0026#34;four\u0026#34;] ) ] ) Recursive Type Definitions\n在F#中，对于类型、标识符的声明和使用顺序有严格规定，使用之前，前方必须有其声明。但是对于就是要互相使用的，就要使用特殊的语法进行处理。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // represents an XML attribute type XmlAttribute = { AttribName: string; AttribValue: string; } // represents an XML element type XmlElement = { ElementName: string; Attributes: list\u0026lt;XmlAttribute\u0026gt;; InnerXml: XmlTree } // represents an XML tree and XmlTree = | Element of XmlElement | ElementList of list\u0026lt;XmlTree\u0026gt; // or: ElementList of XmlTree list | Text of string | Comment of string | Empty Active Patterns 主动匹配，允许我们对于输入的数据进行分类并分类命名，然后结合模式匹配表达式进行分别处理。\n主动匹配分为完全主动匹配和部分主动匹配，他们可以在多处定义，然后在某个模式匹配表达式中同时使用上。\nComplete Active Patterns\n完全主动匹配就是对于输入的输入进行完全的分类，每个类型都显式给出一个名字，它跟部分主动匹配最大的不同就是：保证给出一个值。如下例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 open System // definition of the active pattern let (|MyBoolean|MyInt|MyFloat|MyString|) input = let success, res = Boolean.TryParse input if success then MyBoolean(res) else let success, res = Int32.TryParse input if success then MyInt(res) else let success, res = Double.TryParse input if success then MyFloat(res) else MyString(input) let printInputWithType input = match input with | MyBoolean b -\u0026gt; printfn \u0026#34;Boolean: %b\u0026#34; b | MyInt i -\u0026gt; printfn \u0026#34;Int: %i\u0026#34; i | MyFloat f -\u0026gt; printfn \u0026#34;Float: %f\u0026#34; f | MyString s -\u0026gt; printfn \u0026#34;String: %s\u0026#34; s Partially Active Patterns\n部分主动匹配可以用于测试某个输入是不是某个类型的值，如果是那么给出Some(Value)若不是，那么None\n1 2 3 4 5 6 7 8 9 10 11 12 let (|Integer|_|) str = if str=\u0026#34;\u0026#34; then Some(1) else None let (|Float|_|) str = if str=\u0026#34;.\u0026#34; then Some(1.0) else None let tem str = match str with | Integer i -\u0026gt; printf \u0026#34;%d\u0026#34; i | Float f -\u0026gt; printf \u0026#34;%f\u0026#34; f | _ -\u0026gt; printf \u0026#34;XXXXXX\u0026#34; Units of Measure 测量单位是特意加入到F#的类型系统中的有趣特性。关于它的使用看语法就能明白了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // define some units of measure [\u0026lt;Measure\u0026gt;]type liter [\u0026lt;Measure\u0026gt;]type pint // define some volumes let vol1 = 2.5\u0026lt;liter\u0026gt; let vol2 = 2.5\u0026lt;pint\u0026gt; // define the ratio of pints to liters let ratio = 1.0\u0026lt;liter\u0026gt; / 1.76056338\u0026lt;pint\u0026gt; // a function to convert pints to liters let convertPintToLiter pints = pints * ratio // perform the conversion and add the values let newVol = vol1 + (convertPintToLiter vol2) // stripping off unit of measure (\u0026lt;= F# 3.1) printfn \u0026#34;The volume is %f\u0026#34; (float vol1) // using a format placeholder with a unit-of-measure value (\u0026gt;= F# 4.0) printfn \u0026#34;The volume is %f\u0026#34; vol1 Exceptions and Exception Handling F#中的异常和异常处理跟C#还是有些不一样的地方的，可以使用exception关键字自定义异常，然后使用raise关键字触发引发一个异常，也可以使用failwith函数替代raise关键字。failwith函数一般用于引发一个普通的，带有文字描述的异常信息的异常，然后丢到上层去。\n捕获异常不同类型，有些类似模式匹配了\n此外，F#也支持try-finally结构，用于保证某些代码必定会执行（finally语句块无法和with语句块共存，或者说，没有提供try-with-finally语法，一般用try-finally嵌套try-with）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // define an exception type exception WrongSecond of int // list of prime numbers let primes = [ 2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47; 53; 59 ] // function to test if current second is prime let testSecond() = try let currentSecond = System.DateTime.Now.Second in // test if current second is in the list of primes if List.exists (fun x -\u0026gt; x = currentSecond) primes then // use the failwith function to raise an exception failwith \u0026#34;A prime second\u0026#34; else // raise the WrongSecond exception raise (WrongSecond currentSecond) with // catch the wrong second exception WrongSecond x -\u0026gt; printf \u0026#34;The current was %i, which is not prime\u0026#34; x // call the function testSecond() // try-finally // function to write to a file let writeToFile() = // open a file let file = System.IO.File.CreateText(\u0026#34;test.txt\u0026#34;) try // write to it file.WriteLine(\u0026#34;Hello F# users\u0026#34;) finally // close the file, this will happen even if // an exception occurs writing to the file file.Dispose() // call the function writeToFile() Lazy Evaluation 标记为lazy的运算一旦被求值，就会将值缓存起来，下次被调用直接使用缓存的值。如果当中存在任何副作用（输出等），也只会体现一次。\n1 2 3 4 5 6 7 8 9 10 let lazySideEffect = lazy( let temp = 2 + 2 printfn \u0026#34;%i\u0026#34; temp temp) printfn \u0026#34;Force value the first time: \u0026#34; let actualValue1 = Lazy.force lazySideEffect printfn \u0026#34;Force value the second time: \u0026#34; let actualValue2 = Lazy.force lazySideEffect 1 2 3 4 5 6 7 let fibs = Seq.unfold (fun (x0, x1) -\u0026gt; Some(x0, (x1, x0+x1))) (1,1) Seq.take 20 fibs ","date":"2017-07-23T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/language/fsharp/fsharp-note-1/","title":"F#-Notes-1 Functional Programming"},{"content":"通过实现“快排”对比OO\u0026amp;FP 简介 这里只是在对比了，通过用Java/C#及F#实现一个简易版本的快速排序算法，针对语言特性进行对比\n首先描述一下这个简易版本的算法思路：\n如果是个空列表，那就直接返回； 如果不是空的，那就选取第一个元素作为pivot； 从剩余部分选取所有比pivot小的元素，进行排序(递归地)； 选取所有比pivot大的元素进行递归排序； 将三部分组合起来，即 [小的,pivot,大的] 关于如下的各种实现，不关注过多算法细节，比如pivot选取，可能遇到的最坏情况，元素数量少时算法的表现等等\n这里只关注一点，用不同的语言怎样才能描述这个算法，然后直观的观察代码长度，比较语言特性的优劣，注意，只关注语言特性\nIn Java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public static \u0026lt;T extends Comparable\u0026lt;T\u0026gt;\u0026gt; Iterable\u0026lt;T\u0026gt; quicksort(Iterable\u0026lt;T\u0026gt; values) { if(values == null || !values.iterator().hasNext()) return new ArrayList\u0026lt;T\u0026gt;(); // 获取迭代器，并选取第一个元素作为主元 Iterator\u0026lt;T\u0026gt; iterator = values.iterator(); T pivot = iterator.next(); // 将剩余元素划分 List\u0026lt;T\u0026gt; smallers = new ArrayList\u0026lt;T\u0026gt;(); List\u0026lt;T\u0026gt; biggers = new ArrayList\u0026lt;T\u0026gt;(); while (iterator.hasNext()) { T current = iterator.next(); if (pivot.compareTo(current) \u0026gt;= 0) smallers.add(current); if (pivot.compareTo(current) \u0026lt; 0) biggers.add(current); } // 对划分出来的两部分再进行排序 Iterable\u0026lt;T\u0026gt; sortedSmallers = quicksort(smallers); Iterable\u0026lt;T\u0026gt; sortedBiggers = quicksort(biggers); // 构建结果集合 List\u0026lt;T\u0026gt; result = new ArrayList\u0026lt;T\u0026gt;(); // 向List中按顺序添加已经有序的元素 sortedSmallers.forEach(result::add); //方法引用，对lambda表达式的进一步简化 result.add(pivot); sortedBiggers.forEach(result::add); return result; } 在此处，使用了泛型及约束，用以保证对象之间是可比较的，总体思路是获取迭代器然后进行操作，平白直叙，并且使用方法引用等尽量缩减代码，没什么好说的\nIn C# 版本 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static List\u0026lt;T\u0026gt; QuickSort\u0026lt;T\u0026gt;(List\u0026lt;T\u0026gt; values) where T : IComparable // 泛型约束：必须实现IComparable接口 { if (values.Count == 0) return new List\u0026lt;T\u0026gt;(); // 取第一个元素作为pivot T firstElement = values[0]; // 分别为小于和大于等于的元素创建列表 var smallerElements = new List\u0026lt;T\u0026gt;(); var largerElements = new List\u0026lt;T\u0026gt;(); for (int i = 1; i \u0026lt; values.Count; i++) // 下标必须从1开始 { var elem = values[i]; if (elem.CompareTo(firstElement) \u0026lt; 0) smallerElements.Add(elem); else largerElements.Add(elem); } // 构建结果对象并返回 var result = new List\u0026lt;T\u0026gt;(); result.AddRange(QuickSort(smallerElements.ToList())); result.Add(firstElement); result.AddRange(QuickSort(largerElements.ToList())); return result; } 版本2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /// \u0026lt;summary\u0026gt; /// 作为 IEnumerable 接口的扩展方法实现 /// \u0026lt;/summary\u0026gt; public static IEnumerable\u0026lt;T\u0026gt; QuickSort\u0026lt;T\u0026gt;(this IEnumerable\u0026lt;T\u0026gt; values) where T : IComparable { if (values == null || !values.Any()) return new List\u0026lt;T\u0026gt;(); // 把列表分成首元素和剩余元素两部分 var firstElement = values.First(); var rest = values.Skip(1); // 将剩余元素分到两个序列并继续排序 var smallerElements = rest .Where(i =\u0026gt; i.CompareTo(firstElement) \u0026lt; 0) .QuickSort(); // 注意：此处是在递归调用本方法 var largerElements = rest .Where(i =\u0026gt; i.CompareTo(firstElement) \u0026gt;= 0) .QuickSort(); // 返回结果 return smallerElements .Concat(new List\u0026lt;T\u0026gt; { firstElement }) .Concat(largerElements); } 观察如上两个版本，都跟Java一样，加入了泛型及约束。关于版本1，似乎还不错，相对比Java版本的实现简洁了不少，当然由于Java使用的是Iterable接口而不是List/ArrayList 因此有些操作方法并不存在只能用稍微麻烦点的写法，似乎有些欺负Java，不过可以随手实现个List\u0026lt;T\u0026gt;版本，没什么新东西，那就留给大伙了\n不过相对于Java的Iterable接口，C#的对应物是IEnumerable接口，而且尽可能用上C#的语言特性去精简代码，因此版本2看上去似乎就很简练了，而且还稍稍有些函数式的味道的，不过先不要下结论，这还并不是极限\nIn F# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let rec quicksort list = match list with | [] -\u0026gt; [] // 如果是个空列表，那就直接返回 | firstElem::otherElements -\u0026gt; // 如果不是空列表，那就分成 首元素，剩余元素 两部分 let smallerElements = otherElements |\u0026gt; List.filter (fun e -\u0026gt; e \u0026lt; firstElem) //从剩余元素中选取比首元素小的 |\u0026gt; quicksort // 然后进行排序 let largerElements = otherElements |\u0026gt; List.filter (fun e -\u0026gt; e \u0026gt;= firstElem) //从剩余元素中选取大于等于首元素的 |\u0026gt; quicksort // 然后进行排序 // 将三个部分连接成一个新的列表，并返回 List.concat [smallerElements; [firstElem]; largerElements] //test printfn \u0026#34;%A\u0026#34; (quicksort [1;5;23;18;9;1;3]) 该算法实现中涉及到的细节暂不深谈，大体包括了pattern matching、partial application、lambda expression、recursive function等知识，此处暂不讨论，只谈谈感觉\n可以看到，这个版本的代码实现似乎还算是简洁，但是咱们有一说一，真是没什么惊艳的，总体来说，算是还好吧，也实在没什么可圈可点，可以看作是对于C#版本2的另一种描述，看上去只是语法不同了而已\n但是这只是对算法的一种描述方式而已，那么我们换个方式\n1 2 3 4 5 let rec quicksort = function | [] -\u0026gt; [] | first::rest -\u0026gt; let smaller,larger = List.partition ((\u0026gt;=) first) rest List.concat [quicksort smaller; [first]; quicksort larger] 好了，如上五行代码便是对我们简易的快排的描述，最F#，最函数式的描述，看上去便是一堆符号和单词丢在了一起，就完成了快速排序， 嗯，这一个版本，有没有惊艳到。\n相对于上一个，这里只有一处新东西，tuple，后续会讨论\n个人认为，F#（或者其它函数式语言）确实是一个很值得学习的东西，毕竟是在用一个不同于面向对象的思想去构建代码，把各种语言元素、所谓“奇技淫巧”组合在一起，做出奇奇怪怪的事情，刷刷三观么。当然在目前的平台上，函数式的思路写法上确实很优雅，但是在目前对算法进行评价的时间复杂度、空间复杂度这方面，函数式语言描述的算法也确实是有劣势的。\n","date":"2016-11-07T00:00:00+08:00","permalink":"https://blog.mxtao.top/posts/paradigm/functional/quicksort-in-csharp-java-fsharp/","title":"通过实现“快排”对比OO\u0026FP"}]